Work in progress
This commit is contained in:
64
client/components/pages/OpenFormFooter.vue
Normal file
64
client/components/pages/OpenFormFooter.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="grid md:grid-cols-3 my-8">
|
||||
<div class="flex mt-2">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 text-center w-full">
|
||||
© Copyright {{ currYear }}. All Rights Reserved
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center mt-5 md:mt-0">
|
||||
<router-link :to="{ name: user ? 'home' : 'index' }" class="flex-shrink-0 font-semibold flex items-center">
|
||||
<img src="/img/logo.svg" alt="notion tools logo" class="w-10 h-10">
|
||||
|
||||
<span class="ml-2 text-xl text-black dark:text-white">
|
||||
OpnForm
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<ul class="flex justify-center mt-5 md:mt-1">
|
||||
<li class="mr-10">
|
||||
<router-link :to="{name:'privacy-policy'}" class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue">
|
||||
Privacy Policy
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="list-disc pl-3">
|
||||
<router-link :to="{name:'terms-conditions'}" class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue">
|
||||
Terms & Conditions
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
user : computed(() => authStore.user)
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
currYear: new Date().getFullYear(),
|
||||
}),
|
||||
|
||||
computed: {
|
||||
helpUrl: () => this.$config.links.help_url,
|
||||
githubUrl: () => this.$config.links.github_url,
|
||||
forumUrl: () => this.$config.links.github_forum_url,
|
||||
changelogUrl: () => this.$config.links.changelog_url,
|
||||
facebookGroupUrl: () => this.$config.links.facebook_group,
|
||||
twitterUrl: () => this.$config.links.twitter,
|
||||
featureRequestsUrl: () => this.$config.links.feature_requests
|
||||
},
|
||||
|
||||
mounted () {},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
78
client/components/pages/auth/ForgotPasswordModal.vue
Normal file
78
client/components/pages/auth/ForgotPasswordModal.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<!-- Forgot password modal -->
|
||||
<modal :show="show" @close="close" max-width="lg">
|
||||
<template #icon>
|
||||
<template v-if="isMailSent">
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="text-blue-50" width="56" height="56" rx="28" fill="currentColor"/>
|
||||
<path d="M16.3333 22.1666L25.859 28.8346C26.6304 29.3746 27.016 29.6446 27.4356 29.7492C27.8061 29.8415 28.1937 29.8415 28.5643 29.7492C28.9838 29.6446 29.3695 29.3746 30.1408 28.8346L39.6666 22.1666M21.9333 37.3333H34.0666C36.0268 37.3333 37.0069 37.3333 37.7556 36.9518C38.4141 36.6163 38.9496 36.0808 39.2851 35.4223C39.6666 34.6736 39.6666 33.6935 39.6666 31.7333V24.2666C39.6666 22.3064 39.6666 21.3264 39.2851 20.5777C38.9496 19.9191 38.4141 19.3837 37.7556 19.0481C37.0069 18.6666 36.0268 18.6666 34.0666 18.6666H21.9333C19.9731 18.6666 18.993 18.6666 18.2443 19.0481C17.5857 19.3837 17.0503 19.9191 16.7147 20.5777C16.3333 21.3264 16.3333 22.3064 16.3333 24.2666V31.7333C16.3333 33.6935 16.3333 34.6736 16.7147 35.4223C17.0503 36.0808 17.5857 36.6163 18.2443 36.9518C18.993 37.3333 19.9731 37.3333 21.9333 37.3333Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="text-blue-50" width="56" height="56" rx="28" fill="currentColor"/>
|
||||
<path d="M33.8333 24.4999C33.8333 23.9028 33.6055 23.3057 33.1499 22.8501C32.6943 22.3945 32.0972 22.1667 31.5 22.1667M31.5 31.5C35.366 31.5 38.5 28.366 38.5 24.5C38.5 20.634 35.366 17.5 31.5 17.5C27.634 17.5 24.5 20.634 24.5 24.5C24.5 24.8193 24.5214 25.1336 24.5628 25.4415C24.6309 25.948 24.6649 26.2013 24.642 26.3615C24.6181 26.5284 24.5877 26.6184 24.5055 26.7655C24.4265 26.9068 24.2873 27.046 24.009 27.3243L18.0467 33.2866C17.845 33.4884 17.7441 33.5893 17.6719 33.707C17.608 33.8114 17.5608 33.9252 17.5322 34.0442C17.5 34.1785 17.5 34.3212 17.5 34.6065V36.6333C17.5 37.2867 17.5 37.6134 17.6272 37.863C17.739 38.0825 17.9175 38.261 18.137 38.3728C18.3866 38.5 18.7133 38.5 19.3667 38.5H22.1667V36.1667H24.5V33.8333H26.8333L28.6757 31.991C28.954 31.7127 29.0932 31.5735 29.2345 31.4945C29.3816 31.4123 29.4716 31.3819 29.6385 31.358C29.7987 31.3351 30.052 31.3691 30.5585 31.4372C30.8664 31.4786 31.1807 31.5 31.5 31.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</template>
|
||||
</template>
|
||||
<template #title>
|
||||
<template v-if="isMailSent">Check your email</template>
|
||||
<template v-else>Forgot password?</template>
|
||||
</template>
|
||||
<template v-if="isMailSent">
|
||||
<div class="text-center">We sent a password reset link to <br/><span>{{form.email}}</span></div>
|
||||
<div class="w-full p-4 text-center">
|
||||
<span class="mt-4">Didn't receive the email? <a href="#" class="ml-1" @click.prevent="send">Click to resend</a></span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center">No worries, we'll send you reset instructions.</div>
|
||||
<form @submit.prevent="send" @keydown="form.onKeydown($event)" class="p-4">
|
||||
<text-input name="email" :form="form" label="Email" placeholder="Your email address" :required="true" />
|
||||
|
||||
<div class="w-full mt-6">
|
||||
<v-button :loading="form.busy" class="w-full my-3">Reset password</v-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<div class="w-full text-center">
|
||||
<a href="#" @click.prevent="close" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
|
||||
<svg class="inline mr-1" width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3334 6.99996H1.66669M1.66669 6.99996L7.50002 12.8333M1.66669 6.99996L7.50002 1.16663" stroke="#475467" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Back to log in
|
||||
</a>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Form from 'vform'
|
||||
|
||||
export default {
|
||||
name: 'ForgotPasswordModal',
|
||||
components: { },
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
isMailSent: false,
|
||||
form: new Form({
|
||||
email: ''
|
||||
})
|
||||
}),
|
||||
methods: {
|
||||
async send () {
|
||||
const { data } = await this.form.post('/api/password/email')
|
||||
this.isMailSent = true
|
||||
},
|
||||
close () {
|
||||
this.$emit('close')
|
||||
this.isMailSent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
115
client/components/pages/auth/components/LoginForm.vue
Normal file
115
client/components/pages/auth/components/LoginForm.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
|
||||
|
||||
<form class="mt-4" @submit.prevent="login" @keydown="form.onKeydown($event)">
|
||||
<!-- Email -->
|
||||
<text-input name="email" :form="form" label="Email" :required="true" placeholder="Your email address" />
|
||||
|
||||
<!-- Password -->
|
||||
<text-input native-type="password" placeholder="Your password"
|
||||
name="password" :form="form" label="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">
|
||||
Remember me
|
||||
</v-checkbox>
|
||||
|
||||
<div class="w-full md:w-1/2 text-right">
|
||||
<a href="#" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700" @click.prevent="showForgotModal=true">
|
||||
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 v-if="isQuick" href="#" class="font-semibold ml-1" @click.prevent="$emit('openRegister')">Sign Up</a>
|
||||
<router-link v-else :to="{name:'register'}" class="font-semibold ml-1">
|
||||
Sign Up
|
||||
</router-link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import Form from 'vform'
|
||||
import Cookies from 'js-cookie'
|
||||
import { useAuthStore } from '../../../../stores/auth.js'
|
||||
import OpenFormFooter from '../../OpenFormFooter.vue'
|
||||
import Testimonials from '../../welcome/Testimonials.vue'
|
||||
import ForgotPasswordModal from '../ForgotPasswordModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'LoginForm',
|
||||
components: {
|
||||
OpenFormFooter,
|
||||
Testimonials,
|
||||
ForgotPasswordModal
|
||||
},
|
||||
props: {
|
||||
isQuick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
authStore
|
||||
}
|
||||
},
|
||||
|
||||
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.authStore.saveToken(data.token, this.remember)
|
||||
|
||||
// Fetch the user.
|
||||
await this.authStore.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>
|
||||
76
client/components/pages/auth/components/QuickRegister.vue
Normal file
76
client/components/pages/auth/components/QuickRegister.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Login modal -->
|
||||
<modal :show="showLoginModal" @close="showLoginModal=false" max-width="lg">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</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.vue'
|
||||
import RegisterForm from './RegisterForm.vue'
|
||||
|
||||
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>
|
||||
154
client/components/pages/auth/components/RegisterForm.vue
Normal file
154
client/components/pages/auth/components/RegisterForm.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div>
|
||||
<form class="mt-4" @submit.prevent="register" @keydown="form.onKeydown($event)">
|
||||
<!-- Name -->
|
||||
<text-input name="name" :form="form" label="Name" placeholder="Your name" :required="true" />
|
||||
|
||||
<!-- Email -->
|
||||
<text-input name="email" :form="form" label="Email" :required="true" placeholder="Your email address" />
|
||||
|
||||
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
|
||||
label="How did you hear about us?" :required="true"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<text-input native-type="password" placeholder="Enter password"
|
||||
name="password" :form="form" label="Password" :required="true"
|
||||
/>
|
||||
|
||||
<!-- Password Confirmation-->
|
||||
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
|
||||
name="password_confirmation" label="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 v-if="isQuick" href="#" class="font-semibold ml-1" @click.prevent="$emit('openLogin')">Log In</a>
|
||||
<router-link v-else :to="{name:'login'}" class="font-semibold ml-1">
|
||||
Log In
|
||||
</router-link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import Form from 'vform'
|
||||
import { useAuthStore } from '../../../../stores/auth.js'
|
||||
import { initCrisp } from '../../../middleware/check-auth.js'
|
||||
|
||||
export default {
|
||||
name: 'RegisterForm',
|
||||
components: {},
|
||||
props: {
|
||||
isQuick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
authStore
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
form: new Form({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
agree_terms: false,
|
||||
appsumo_license: null
|
||||
}),
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
// Set appsumo license
|
||||
if (this.$route.query.appsumo_license !== undefined && this.$route.query.appsumo_license) {
|
||||
this.form.appsumo_license = this.$route.query.appsumo_license
|
||||
}
|
||||
},
|
||||
|
||||
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.authStore.saveToken(token)
|
||||
|
||||
// Update the user.
|
||||
await this.authStore.updateUser(data)
|
||||
|
||||
// Track event
|
||||
this.$logEvent('register', { source: this.form.hear_about_us })
|
||||
|
||||
initCrisp(data)
|
||||
this.$crisp.push(['set', 'session:event', [[['register', {}, 'blue']]]])
|
||||
|
||||
// AppSumo License
|
||||
if (data.appsumo_license === false) {
|
||||
this.alertError('Invalid AppSumo license. This probably happened because this license was already' +
|
||||
' attached to another OpnForm account. Please contact support.')
|
||||
} else if (data.appsumo_license === true) {
|
||||
this.alertSuccess('Your AppSumo license was successfully activated! You now have access to all the' +
|
||||
' features of the AppSumo deal.')
|
||||
}
|
||||
|
||||
// Redirect
|
||||
if (this.isQuick) {
|
||||
this.$emit('afterQuickLogin')
|
||||
} else {
|
||||
this.$router.push({ name: 'forms.create' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
107
client/components/pages/forms/NewFeatures.vue
Normal file
107
client/components/pages/forms/NewFeatures.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div id="new-features"
|
||||
class="w-full bg-gray-50 dark:bg-gray-800 border rounded-lg mt-4"
|
||||
>
|
||||
<div class="border-b">
|
||||
<div v-track.new_in_notionforms_click
|
||||
class="relative flex items-center cursor-pointer hover:bg-gray-100 p-4" role="button"
|
||||
@click.prevent="showNewFeatures=!showNewFeatures"
|
||||
>
|
||||
<div class="text-gray-700 dark:text-gray-300 pr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 dark:text-gray-300 font-semibold">
|
||||
New in OpnForm
|
||||
</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Click here to see our new features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-transition>
|
||||
<ul v-if="showNewFeatures" class="list-disc list-inside border-t pt-2 p-4">
|
||||
<li v-for="changelog in changelogEntries" :key="changelog.id" v-track.new_feature_click class="text-sm">
|
||||
<a :href="changelog.url" target="_blank" class="text-gray-700 dark:text-gray-300">{{ changelog.title }}</a>
|
||||
</li>
|
||||
<li v-track.new_feature_read_more_click class="text-sm">
|
||||
<a class="text-gray-700 dark:text-gray-300" :href="changelogLink" target="_blank">Read more</a>
|
||||
</li>
|
||||
</ul>
|
||||
</v-transition>
|
||||
</div>
|
||||
<div class="relative flex items-center cursor-pointer hover:bg-gray-100 p-4">
|
||||
<a v-track.feature_request_click="{user_has_forms:user.has_forms}" :href="requestFeatureLink"
|
||||
class="absolute inset-0" target="_blank"
|
||||
/>
|
||||
<div class="text-gray-700 dark:text-gray-300 pr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 dark:text-gray-300 font-semibold">
|
||||
An idea for a new feature?
|
||||
</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Click here to request a new feature
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
import VTransition from '~/components/global/transitions/VTransition.vue'
|
||||
|
||||
export default {
|
||||
components: { VTransition },
|
||||
props: {},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
user : computed(() => authStore.user)
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
changelogEntries: [],
|
||||
showNewFeatures: false
|
||||
}),
|
||||
|
||||
mounted () {
|
||||
this.loadChangelogEntries()
|
||||
},
|
||||
|
||||
computed: {
|
||||
requestFeatureLink () {
|
||||
return this.$config.links.feature_requests
|
||||
},
|
||||
changelogLink () {
|
||||
return this.$config.links.changelog_url
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadChangelogEntries () {
|
||||
axios.get('/api/content/changelog/entries').then(response => {
|
||||
this.changelogEntries = response.data.splice(0, 3)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
157
client/components/pages/forms/create/CreateFormBaseModal.vue
Normal file
157
client/components/pages/forms/create/CreateFormBaseModal.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<modal :show="show" :closeable="!aiForm.busy" @close="$emit('close')">
|
||||
<template #icon>
|
||||
<template v-if="state=='default'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10 text-blue">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="state=='ai'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path fill-rule="evenodd"
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</template>
|
||||
<template #title>
|
||||
<template v-if="state=='default'">
|
||||
Choose a base for your form
|
||||
</template>
|
||||
<template v-else-if="state=='ai'">
|
||||
AI-powered form generator
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="state=='default'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
<div v-track.select_form_base="{base:'contact-form'}"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="$emit('close')"
|
||||
>
|
||||
<div class="p-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" />
|
||||
<path d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
Start from a simple contact form
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="aiFeaturesEnabled" v-track.select_form_base="{base:'ai'}"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="state='ai'"
|
||||
>
|
||||
<div class="p-4 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path fill-rule="evenodd"
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium text-blue-700">
|
||||
Use our AI to create the form
|
||||
</p>
|
||||
<span class="text-xs text-gray-500">(1 min)</span>
|
||||
</div>
|
||||
<div class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50 relative">
|
||||
<div class="p-4 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path
|
||||
d="M11.25 5.337c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.036 1.007-1.875 2.25-1.875S15 2.34 15 3.375c0 .369-.128.713-.349 1.003-.215.283-.401.604-.401.959 0 .332.278.598.61.578 1.91-.114 3.79-.342 5.632-.676a.75.75 0 01.878.645 49.17 49.17 0 01.376 5.452.657.657 0 01-.66.664c-.354 0-.675-.186-.958-.401a1.647 1.647 0 00-1.003-.349c-1.035 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401.31 0 .557.262.534.571a48.774 48.774 0 01-.595 4.845.75.75 0 01-.61.61c-1.82.317-3.673.533-5.555.642a.58.58 0 01-.611-.581c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.035-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959a.641.641 0 01-.658.643 49.118 49.118 0 01-4.708-.36.75.75 0 01-.645-.878c.293-1.614.504-3.257.629-4.924A.53.53 0 005.337 15c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.036 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.369 0 .713.128 1.003.349.283.215.604.401.959.401a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.75.75 0 01.83-.832c1.343.155 2.703.254 4.077.294a.64.64 0 00.657-.642z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
Start from a template
|
||||
</p>
|
||||
<router-link v-track.select_form_base="{base:'template'}" :to="{name:'templates'}" class="absolute inset-0" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="state=='ai'">
|
||||
<a class="absolute top-4 left-4" href="#" @click.prevent="state='default'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 inline -mt-1">
|
||||
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<text-area-input label="Form Description" :disabled="loading?true:null" :form="aiForm" name="form_prompt" help="Give us a description of the form you want to build (the more details the better)"
|
||||
placeholder="A simple contact form, with a name, email and message field"
|
||||
/>
|
||||
<v-button class="w-full" :loading="loading" @click.prevent="generateForm">
|
||||
Generate a form
|
||||
</v-button>
|
||||
<p class="text-gray-500 text-xs text-center mt-1">
|
||||
~60 sec
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loader from '~/components/global/Loader.vue'
|
||||
import Form from 'vform'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'CreateFormBaseModal',
|
||||
components: { Loader },
|
||||
props: {
|
||||
show: { type: Boolean, required: true }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
state: 'default',
|
||||
aiForm: new Form({
|
||||
form_prompt: ''
|
||||
}),
|
||||
loading: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
aiFeaturesEnabled () {
|
||||
return this.$config.ai_features_enabled
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateForm () {
|
||||
if (this.loading) return
|
||||
|
||||
this.loading = true
|
||||
this.aiForm.post('/api/forms/ai/generate').then(response => {
|
||||
this.alertSuccess(response.data.message)
|
||||
this.fetchGeneratedForm(response.data.ai_form_completion_id)
|
||||
}).catch(error => {
|
||||
this.alertError(error.response.data.message)
|
||||
this.loading = false
|
||||
this.state = 'default'
|
||||
})
|
||||
},
|
||||
fetchGeneratedForm (generationId) {
|
||||
// check every 4 seconds if form is generated
|
||||
setTimeout(() => {
|
||||
axios.get('/api/forms/ai/' + generationId).then(response => {
|
||||
if (response.data.ai_form_completion.status === 'completed') {
|
||||
this.alertSuccess(response.data.message)
|
||||
this.$emit('form-generated', JSON.parse(response.data.ai_form_completion.result))
|
||||
this.$emit('close')
|
||||
} else if (response.data.ai_form_completion.status === 'failed') {
|
||||
this.alertError('Something went wrong, please try again.')
|
||||
this.state = 'default'
|
||||
this.loading = false
|
||||
} else {
|
||||
this.fetchGeneratedForm(generationId)
|
||||
}
|
||||
}).catch(error => {
|
||||
this.alertError(error.response.data.message)
|
||||
this.state = 'default'
|
||||
this.loading = false
|
||||
})
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
55
client/components/pages/forms/show/EmbedCode.vue
Normal file
55
client/components/pages/forms/show/EmbedCode.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="font-semibold text-xl">Embed</h3>
|
||||
<p>Embed your form on your website by copying the HTML code below.</p>
|
||||
<copy-content :content="embedCode" buttonText="Copy Code">
|
||||
<template #icon>
|
||||
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 18 18" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M11.0833 11.5L13.5833 9L11.0833 6.5M6.91667 6.5L4.41667 9L6.91667 11.5M5.5 16.5H12.5C13.9001 16.5 14.6002 16.5 15.135 16.2275C15.6054 15.9878 15.9878 15.6054 16.2275 15.135C16.5 14.6002 16.5 13.9001 16.5 12.5V5.5C16.5 4.09987 16.5 3.3998 16.2275 2.86502C15.9878 2.39462 15.6054 2.01217 15.135 1.77248C14.6002 1.5 13.9001 1.5 12.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V12.5C1.5 13.9001 1.5 14.6002 1.77248 15.135C2.01217 15.6054 2.39462 15.9878 2.86502 16.2275C3.3998 16.5 4.09987 16.5 5.5 16.5Z"
|
||||
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</template>
|
||||
Copy Code
|
||||
</copy-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CopyContent from '../../../open/forms/components/CopyContent.vue'
|
||||
|
||||
export default {
|
||||
name: 'EmbedCode',
|
||||
components: { CopyContent },
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
extraQueryParam: { type: String, default: '' }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
||||
}),
|
||||
|
||||
computed: {
|
||||
embedCode() {
|
||||
const share_url = (this.extraQueryParam) ? this.form.share_url + "?" + this.extraQueryParam : this.form.share_url + this.extraQueryParam
|
||||
return '<iframe style="border:none;width:100%;" height="' + this.formHeight + 'px" src="' + share_url + '"></iframe>'
|
||||
},
|
||||
formHeight() {
|
||||
let height = 200
|
||||
if (!this.form.hide_title && !this.extraQueryParam) {
|
||||
height += 60
|
||||
}
|
||||
height += this.form.properties.filter((property) => {
|
||||
return !property.hidden
|
||||
}).length * 70
|
||||
|
||||
return height
|
||||
}
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
199
client/components/pages/forms/show/EmbedFormAsPopupModal.vue
Normal file
199
client/components/pages/forms/show/EmbedFormAsPopupModal.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-button
|
||||
v-track.share_embed_form_popup_click="{form_id:form.id, form_slug:form.slug}"
|
||||
class="w-full"
|
||||
color="light-gray"
|
||||
@click="showEmbedFormAsPopupModal=true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 text-blue-600 inline" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>
|
||||
</svg>
|
||||
Embed form as popup
|
||||
</v-button>
|
||||
|
||||
<modal :show="showEmbedFormAsPopupModal" @close="onClose">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template #title>
|
||||
<span>Add the popup to your website</span>
|
||||
</template>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="border-t text-xl font-semibold mb-2 pt-6">
|
||||
Demo
|
||||
</h3>
|
||||
<p class="pb-6">
|
||||
A live preview of your form popup was just added to this page. <span class="font-semibold text-blue-800">Click on the button on the bottom
|
||||
{{ advancedOptions.position }} corner to try it</span>.
|
||||
</p>
|
||||
|
||||
<h3 class="border-t text-xl font-semibold mb-2 pt-6">
|
||||
How does it work?
|
||||
</h3>
|
||||
<p>Paste the following code snippet in the <b><head></b> section of your website.</p>
|
||||
|
||||
<div
|
||||
class="border border-nt-blue-light bg-blue-50 dark:bg-notion-dark-light rounded-md p-4 mb-5 w-full mx-auto mt-4 select-all"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<p class="select-all text-nt-blue flex-grow break-all">
|
||||
{{ embedPopupCode }}
|
||||
</p>
|
||||
<div class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer" @click="copyToClipboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-nt-blue" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<collapse class="py-5 w-full border rounded-md px-4" :model-value="false">
|
||||
<template #title>
|
||||
<div class="flex">
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Advanced options
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="border-t mt-4 -mx-4" />
|
||||
<toggle-switch-input v-model="advancedOptions.hide_title" name="hide_title" class="mt-4"
|
||||
label="Hide Form Title"
|
||||
:disabled="(form.hide_title===true)?true:null"
|
||||
:help="hideTitleHelp"
|
||||
/>
|
||||
<color-input v-model="advancedOptions.bgcolor" name="bgcolor" class="mt-4"
|
||||
label="Circle Background Color"
|
||||
/>
|
||||
<text-input v-model="advancedOptions.emoji" name="emoji" class="mt-4"
|
||||
label="Emoji" :max-char-limit="2"
|
||||
/>
|
||||
<flat-select-input v-model="advancedOptions.position" name="position" class="mt-4"
|
||||
label="Position"
|
||||
:options="[
|
||||
{name:'Bottom Right',value:'right'},
|
||||
{name:'Bottom Left',value:'left'},
|
||||
]"
|
||||
/>
|
||||
<text-input v-model="advancedOptions.width" name="width" class="mt-4"
|
||||
label="Form pop max width (px)" native-type="number"
|
||||
/>
|
||||
</collapse>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<v-button color="gray" shade="light" @click="onClose">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Collapse from '~/components/global/Collapse.vue'
|
||||
|
||||
export default {
|
||||
name: 'EmbedFormAsPopupModal',
|
||||
components: { Collapse },
|
||||
props: {
|
||||
form: { type: Object, required: true }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
showEmbedFormAsPopupModal: false,
|
||||
embedScriptUrl: 'widgets/embed-min.js',
|
||||
advancedOptions: {
|
||||
hide_title: false,
|
||||
emoji: '💬',
|
||||
position: 'right',
|
||||
bgcolor: '#3B82F6',
|
||||
width: '500'
|
||||
}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
hideTitleHelp () {
|
||||
return this.form.hide_title ? 'This option is disabled because the form title is already hidden' : null
|
||||
},
|
||||
shareUrl () {
|
||||
return (this.advancedOptions.hide_title) ? this.form.share_url + '?hide_title=true' : this.form.share_url
|
||||
},
|
||||
embedPopupCode () {
|
||||
const nfData = {
|
||||
formurl: this.shareUrl,
|
||||
emoji: this.advancedOptions.emoji,
|
||||
position: this.advancedOptions.position,
|
||||
bgcolor: this.advancedOptions.bgcolor,
|
||||
width: this.advancedOptions.width
|
||||
}
|
||||
this.previewPopup(nfData)
|
||||
return '<script async data-nf=\'' + JSON.stringify(nfData) + '\' src=\'' + this.asset(this.embedScriptUrl) + '\'></scrip' + 't>'
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.advancedOptions.bgcolor = this.form.color
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClose () {
|
||||
this.removePreview()
|
||||
this.$crisp.push(['do', 'chat:show'])
|
||||
this.showEmbedFormAsPopupModal = false
|
||||
},
|
||||
copyToClipboard () {
|
||||
if (process.server) return
|
||||
const str = this.embedPopupCode
|
||||
const el = document.createElement('textarea')
|
||||
el.value = str
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
},
|
||||
removePreview () {
|
||||
if (process.server) return
|
||||
const oldP = document.head.querySelector('#nf-popup-preview')
|
||||
if (oldP) {
|
||||
oldP.remove()
|
||||
}
|
||||
const oldM = document.body.querySelector('.nf-main')
|
||||
if (oldM) {
|
||||
oldM.remove()
|
||||
}
|
||||
},
|
||||
previewPopup (nfData) {
|
||||
if (process.server) return
|
||||
if (!this.showEmbedFormAsPopupModal) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove old preview, if there
|
||||
this.removePreview()
|
||||
|
||||
// Hide crisp
|
||||
this.$crisp.push(['do', 'chat:hide'])
|
||||
|
||||
// Add new preview
|
||||
const el = document.createElement('script')
|
||||
el.id = 'nf-popup-preview'
|
||||
el.async = true
|
||||
el.src = this.asset(this.embedScriptUrl)
|
||||
el.setAttribute('data-nf', JSON.stringify(nfData))
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
209
client/components/pages/forms/show/ExtraMenu.vue
Normal file
209
client/components/pages/forms/show/ExtraMenu.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loadingDuplicate || loadingDelete" class="pr-4 pt-2">
|
||||
<loader class="h-6 w-6 mx-auto" />
|
||||
</div>
|
||||
<dropdown v-else class="inline" dusk="nav-dropdown">
|
||||
<template #trigger="{toggle}">
|
||||
<v-button color="white" class="mr-2" @click.stop="toggle">
|
||||
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</v-button>
|
||||
</template>
|
||||
<a v-if="isMainPage && user" v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" :href="form.share_url"
|
||||
target="_blank"
|
||||
class="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"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
View form
|
||||
</a>
|
||||
<router-link v-if="isMainPage" v-track.edit_form_click="{form_id:form.id, form_slug:form.slug}"
|
||||
:to="{name:'forms.edit', params: {slug: form.slug}}"
|
||||
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"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z"
|
||||
stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</router-link>
|
||||
<a v-if="isMainPage" href="#"
|
||||
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="copyLink"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00016 8.33317H4.66683C2.82588 8.33317 1.3335 6.84079 1.3335 4.99984C1.3335 3.15889 2.82588 1.6665 4.66683 1.6665H6.00016M10.0002 8.33317H11.3335C13.1744 8.33317 14.6668 6.84079 14.6668 4.99984C14.6668 3.15889 13.1744 1.6665 11.3335 1.6665H10.0002M4.66683 4.99984L11.3335 4.99984" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Copy link to share
|
||||
</a>
|
||||
<a v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
|
||||
href="#"
|
||||
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="duplicateForm"
|
||||
>
|
||||
<svg class="w-4 h-4 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="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
|
||||
/>
|
||||
</svg>
|
||||
Duplicate form
|
||||
</a>
|
||||
<a v-if="!isMainPage" v-track.create_template_click="{form_id:form.id, form_slug:form.slug}" href="#"
|
||||
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="showFormTemplateModal=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 v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
|
||||
href="#"
|
||||
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center"
|
||||
@click.prevent="showDeleteFormModal=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"
|
||||
>
|
||||
<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>
|
||||
Delete form
|
||||
</a>
|
||||
</dropdown>
|
||||
|
||||
<!-- Delete Form Modal -->
|
||||
<modal :show="showDeleteFormModal" icon-color="red" max-width="sm" @close="showDeleteFormModal=false">
|
||||
<template #icon>
|
||||
<svg class="w-10 h-10" 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="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>
|
||||
</template>
|
||||
<template #title>
|
||||
Delete form
|
||||
</template>
|
||||
<div class="p-3">
|
||||
<p>
|
||||
If you want to permanently delete this form and all of its data, you can do so below.
|
||||
</p>
|
||||
<div class="flex mt-4">
|
||||
<v-button class="sm:w-1/2 mr-4" color="white" @click.prevent="showDeleteFormModal=false">
|
||||
Cancel
|
||||
</v-button>
|
||||
<v-button class="sm:w-1/2" color="red" :loading="loadingDelete" @click.prevent="deleteForm">
|
||||
Yes, delete it
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<form-template-modal v-if="!isMainPage && user" :form="form" :show="showFormTemplateModal" @close="showFormTemplateModal=false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '../../../../stores/auth'
|
||||
import { useFormsStore } from '../../../../stores/forms'
|
||||
import Dropdown from '~/components/global/Dropdown.vue'
|
||||
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'ExtraMenu',
|
||||
components: { Dropdown, FormTemplateModal },
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
isMainPage: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
const formsStore = useFormsStore()
|
||||
return {
|
||||
formsStore,
|
||||
user: computed(() => authStore.user)
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loadingDuplicate: false,
|
||||
loadingDelete: false,
|
||||
showDeleteFormModal: false,
|
||||
showFormTemplateModal: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formEndpoint: () => '/api/open/forms/{id}'
|
||||
},
|
||||
|
||||
methods: {
|
||||
copyLink () {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = this.form.share_url
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
this.alertSuccess('Copied!')
|
||||
},
|
||||
duplicateForm () {
|
||||
if (this.loadingDuplicate) return
|
||||
this.loadingDuplicate = true
|
||||
axios.post(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate').then((response) => {
|
||||
this.formsStore.addOrUpdate(response.data.new_form)
|
||||
this.$router.push({ name: 'forms.show', params: { slug: response.data.new_form.slug } })
|
||||
this.alertSuccess('Form was successfully duplicated.')
|
||||
this.loadingDuplicate = false
|
||||
})
|
||||
},
|
||||
deleteForm () {
|
||||
if (this.loadingDelete) return
|
||||
this.loadingDelete = true
|
||||
axios.delete(this.formEndpoint.replace('{id}', this.form.id)).then(() => {
|
||||
this.formsStore.remove(this.form)
|
||||
this.$router.push({ name: 'home' })
|
||||
this.alertSuccess('Form was deleted.')
|
||||
this.loadingDelete = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
107
client/components/pages/forms/show/FormCleanings.vue
Normal file
107
client/components/pages/forms/show/FormCleanings.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<v-transition>
|
||||
<div v-if="hasCleanings && !hideWarning" class="border border-gray-300 dark:border-gray-600 rounded-md bg-white p-2"
|
||||
:class="{'hover:bg-yellow-50 dark:hover:bg-yellow-900':!collapseOpened}"
|
||||
>
|
||||
<collapse v-model="collapseOpened">
|
||||
<template #title>
|
||||
<p class="text-yellow-500 dark:text-yellow-400 font-semibold text-sm p-1 pr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6 inline"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
Some features that are included in our {{ form.is_pro ? 'Enterprise' : 'Pro' }} plan are disabled when
|
||||
publicly sharing this form<span v-if="specifyFormOwner"> (only owners of this form can see this)</span>.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div class="border-t mt-1 p-4 pb-2 -mx-2">
|
||||
<p class="text-gray-500 text-sm" v-html="cleaningContent" />
|
||||
<p class="text-gray-500 text-sm mb-4 font-semibold">
|
||||
<router-link :to="{name:'pricing'}">
|
||||
{{ form.is_pro ? 'Upgrade your OpnForms plan today' : 'Start your free OpnForms trial' }}
|
||||
</router-link>
|
||||
to unlock all of our features and build powerful forms.
|
||||
</p>
|
||||
<div class="flex flex-wrap items-end w-full">
|
||||
<div class="flex-grow flex pr-2">
|
||||
<v-button v-track.upgrade_from_form_cleanings_click size="small" class="inline-block" :to="{name:'pricing'}">
|
||||
{{ form.is_pro ? 'Upgrade plan' : 'Start free trial' }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-4 h-4 inline -mt-[2px]"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</v-button>
|
||||
<v-button color="white" size="small" class="ml-2" @click.prevent="openCrisp">
|
||||
Contact us
|
||||
</v-button>
|
||||
</div>
|
||||
<v-button v-if="hideable" color="white" size="small" class="mt-2" @click.prevent="hideWarning=true">
|
||||
Hide warning
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</collapse>
|
||||
</div>
|
||||
</v-transition>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import Collapse from '~/components/global/Collapse.vue'
|
||||
import VButton from '~/components/global/VButton.vue'
|
||||
import VTransition from '~/components/global/transitions/VTransition.vue'
|
||||
|
||||
export default {
|
||||
name: 'FormCleanings',
|
||||
components: { VTransition, VButton, Collapse },
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
specifyFormOwner: { type: Boolean, default: false },
|
||||
hideable: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
collapseOpened: false,
|
||||
hideWarning: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasCleanings () {
|
||||
return this.form.cleanings && Object.keys(this.form.cleanings).length > 0
|
||||
},
|
||||
cleanings () {
|
||||
return this.form.cleanings
|
||||
},
|
||||
cleaningContent () {
|
||||
let message = ''
|
||||
Object.keys(this.cleanings).forEach((key) => {
|
||||
let fieldName = key.charAt(0).toUpperCase() + key.slice(1)
|
||||
if (fieldName !== 'Form') {
|
||||
fieldName = '"' + fieldName + '" field'
|
||||
}
|
||||
let fieldInfo = '<span class="font-semibold">' + fieldName + '</span><br/><ul class=\'list-disc list-inside\'>'
|
||||
this.cleanings[key].forEach((msg) => {
|
||||
fieldInfo = fieldInfo + '<li>' + msg + '</li>'
|
||||
})
|
||||
if (fieldInfo) {
|
||||
message = message + fieldInfo + '<ul/><br/>'
|
||||
}
|
||||
})
|
||||
return message
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
openCrisp () {
|
||||
this.$crisp.push(['do', 'chat:show'])
|
||||
this.$crisp.push(['do', 'chat:open'])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
50
client/components/pages/forms/show/FormQrCode.vue
Normal file
50
client/components/pages/forms/show/FormQrCode.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="font-semibold text-xl">QR Code</h3>
|
||||
<p>Scan the QR code to open the form (Right click to copy the image)</p>
|
||||
<div class="flex items-center">
|
||||
<img v-if="QrUrl" :src="QrUrl" class="m-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QRCode from 'qrcode'
|
||||
export default {
|
||||
name: 'FormQrCode',
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
extraQueryParam: { type: String, default: '' }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
QrUrl: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
shareUrl () {
|
||||
return (this.extraQueryParam) ? this.form.share_url + "?" + this.extraQueryParam : this.form.share_url + this.extraQueryParam
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
shareUrl () {
|
||||
this.generateQR()
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.generateQR()
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateQR () {
|
||||
QRCode.toDataURL(this.shareUrl).then(url => {
|
||||
this.QrUrl = url
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
117
client/components/pages/forms/show/RegenerateFormLink.vue
Normal file
117
client/components/pages/forms/show/RegenerateFormLink.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-button
|
||||
class="w-full"
|
||||
color="light-gray"
|
||||
v-track.regenerate_form_link_click="{form_id:form.id, form_slug:form.slug}"
|
||||
@click="showGenerateFormLinkModal=true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 text-blue-600 inline" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
Regenerate form link
|
||||
</v-button>
|
||||
|
||||
<!-- Regenerate form link modal -->
|
||||
<modal :show="showGenerateFormLinkModal" @close="showGenerateFormLinkModal=false">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-600" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template #title>
|
||||
Generate new form link
|
||||
</template>
|
||||
<div class="p-4">
|
||||
<p>
|
||||
You can choose between two different URL formats for your form.
|
||||
<span class="font-semibold">Be careful, changing your form URL is not a reversible operation</span>.
|
||||
Make sure to udpate your form URL everywhere where it's used.
|
||||
</p>
|
||||
<div class="border-t py-4 mt-4">
|
||||
<h3 class="text-xl text-gray-700 font-semibold">
|
||||
Human Readable URL
|
||||
</h3>
|
||||
<p>If your users are going to see this url, you might want to make nice and readable. Example:</p>
|
||||
<p class="text-gray-600 border p-4 bg-gray-50 rounded-md mt-4">
|
||||
https://opnform.com/forms/contact
|
||||
</p>
|
||||
<div class="text-center mt-4">
|
||||
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('slug')">
|
||||
Generate a Human Readable URL
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<h3 class="text-xl text-gray-700 font-semibold">
|
||||
Random ID URL
|
||||
</h3>
|
||||
<p>
|
||||
If your user are not going to see your form url (if it's embedded), and if you prefer to have a random
|
||||
non-guessable URL. Example:
|
||||
</p>
|
||||
<p class="text-gray-600 p-4 border bg-gray-50 rounded-md mt-4">
|
||||
https://opnform.com/forms/b4417f9c-34ae-4421-8006-832ee47786e7
|
||||
</p>
|
||||
<div class="text-center mt-4">
|
||||
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('uuid')">
|
||||
Generate a Random ID URL
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useFormsStore } from '../../../../stores/forms'
|
||||
|
||||
export default {
|
||||
name: 'RegenerateFormLink',
|
||||
components: {},
|
||||
props: {
|
||||
form: { type: Object, required: true }
|
||||
},
|
||||
|
||||
setup () {
|
||||
const formsStore = useFormsStore()
|
||||
return {
|
||||
formsStore
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loadingNewLink: false,
|
||||
showGenerateFormLinkModal: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formEndpoint: () => '/api/open/forms/{id}',
|
||||
},
|
||||
|
||||
methods: {
|
||||
regenerateLink(option) {
|
||||
if (this.loadingNewLink) return
|
||||
this.loadingNewLink = true
|
||||
axios.put(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option).then((response) => {
|
||||
this.formsStore.addOrUpdate(response.data.form)
|
||||
this.$router.push({name: 'forms.show', params: {slug: response.data.form.slug}})
|
||||
this.alertSuccess(response.data.message)
|
||||
this.loadingNewLink = false
|
||||
}).finally(() => {
|
||||
this.showGenerateFormLinkModal = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
44
client/components/pages/forms/show/ShareLink.vue
Normal file
44
client/components/pages/forms/show/ShareLink.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="font-semibold text-xl">Share Link</h3>
|
||||
<p>Your form is now published and ready to be shared with the world! Copy this link to share your form
|
||||
on social media, messaging apps or via email.</p>
|
||||
<copy-content :content="share_url" :is-draft="form.visibility=='draft'">
|
||||
<template #icon>
|
||||
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 20 10" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.49984 9.16634H5.83317C3.53198 9.16634 1.6665 7.30086 1.6665 4.99967C1.6665 2.69849 3.53198 0.833008 5.83317 0.833008H7.49984M12.4998 9.16634H14.1665C16.4677 9.16634 18.3332 7.30086 18.3332 4.99967C18.3332 2.69849 16.4677 0.833008 14.1665 0.833008H12.4998M5.83317 4.99967L14.1665 4.99968"
|
||||
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</template>
|
||||
Copy Link
|
||||
</copy-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CopyContent from '../../../open/forms/components/CopyContent.vue'
|
||||
|
||||
export default {
|
||||
name: 'ShareLink',
|
||||
components: { CopyContent },
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
extraQueryParam: { type: String, default: '' }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
||||
}),
|
||||
|
||||
computed: {
|
||||
share_url () {
|
||||
return (this.extraQueryParam) ? this.form.share_url + '?' + this.extraQueryParam : this.form.share_url + this.extraQueryParam
|
||||
}
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
99
client/components/pages/forms/show/UrlFormPrefill.vue
Normal file
99
client/components/pages/forms/show/UrlFormPrefill.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<v-button
|
||||
class="w-full"
|
||||
color="light-gray"
|
||||
v-track.url_form_prefill_click="{form_id:form.id, form_slug:form.slug}"
|
||||
@click="showUrlFormPrefillModal=true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 text-blue-600 inline" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2h2m3-4H9a2 2 0 00-2 2v7a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-1m-1 4l-3 3m0 0l-3-3m3 3V3"
|
||||
/>
|
||||
</svg>
|
||||
Url form pre-fill
|
||||
</v-button>
|
||||
|
||||
<modal :show="showUrlFormPrefillModal" @close="showUrlFormPrefillModal=false">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2h2m3-4H9a2 2 0 00-2 2v7a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-1m-1 4l-3 3m0 0l-3-3m3 3V3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template #title>
|
||||
<span>Url Form Prefill</span>
|
||||
</template>
|
||||
|
||||
<div class="p-4" ref="content">
|
||||
<p>
|
||||
Create dynamic links when sharing your form (whether it's embedded or not), that allows you to prefill
|
||||
your form fields. You can use this to personalize the form when sending it to multiple contacts for instance.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-6 border-t text-xl font-semibold mb-4 pt-6">
|
||||
How does it work?
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Complete your form below and fill only the fields you want to prefill. You can even leave the required fields empty.
|
||||
</p>
|
||||
|
||||
<div class="rounded-lg p-5 bg-gray-100 dark:bg-gray-900 mt-4">
|
||||
<open-form v-if="form" :theme="theme" :loading="false" :show-hidden="true" :form="form" :fields="form.properties" @submit="generateUrl">
|
||||
<template #submit-btn="{submitForm}">
|
||||
<v-button class="mt-2 px-8 mx-1" @click.prevent="submitForm">
|
||||
Generate Pre-filled URL
|
||||
</v-button>
|
||||
</template>
|
||||
</open-form>
|
||||
</div>
|
||||
|
||||
<template v-if="prefillFormData">
|
||||
<h3 class="mt-6 text-xl font-semibold mb-4 pt-6">
|
||||
Your Prefill url
|
||||
</h3>
|
||||
<form-url-prefill :form="form" :form-data="prefillFormData" :extra-query-param="extraQueryParam" />
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormUrlPrefill from '../../../open/forms/components/FormUrlPrefill.vue'
|
||||
import ProTag from '~/components/global/ProTag.vue'
|
||||
import OpenForm from '../../../open/forms/OpenForm.vue'
|
||||
import { themes } from '~/config/form-themes.js'
|
||||
|
||||
export default {
|
||||
name: 'UrlFormPrefill',
|
||||
components: { FormUrlPrefill, ProTag, OpenForm },
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
extraQueryParam: { type: String, default: '' }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
prefillFormData: null,
|
||||
theme: themes.default,
|
||||
showUrlFormPrefillModal: false,
|
||||
}),
|
||||
|
||||
computed: {},
|
||||
|
||||
methods: {
|
||||
generateUrl (formData, onFailure) {
|
||||
this.prefillFormData = formData
|
||||
this.$nextTick().then(() => {
|
||||
if (this.$refs.content) {
|
||||
this.$refs.content.parentElement.parentElement.parentElement.scrollTop = this.$refs.content.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
101
client/components/pages/pricing/CheckoutDetailsModal.vue
Normal file
101
client/components/pages/pricing/CheckoutDetailsModal.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<modal :show="show" max-width="lg" @close="close">
|
||||
<text-input ref="companyName" label="Company Name" name="name" :required="true" :form="form" help="Name that will appear on invoices" />
|
||||
<text-input label="Email" name="email" native-type="email" :required="true" :form="form" help="Where invoices will be sent" />
|
||||
<v-button :loading="form.busy || loading" :disabled="(form.busy || loading)?true:null" class="mt-6 block mx-auto"
|
||||
:arrow="true" @click="saveDetails"
|
||||
>
|
||||
Go to checkout
|
||||
</v-button>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Form from 'vform'
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
import TextInput from '../../forms/TextInput.vue'
|
||||
import VButton from '~/components/global/VButton.vue'
|
||||
|
||||
export default {
|
||||
components: { VButton, TextInput },
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
plan: {
|
||||
type: String,
|
||||
default: 'pro'
|
||||
},
|
||||
yearly: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
user: computed(() => authStore.user)
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
form: new Form({
|
||||
name: '',
|
||||
email: ''
|
||||
}),
|
||||
loading: false
|
||||
}),
|
||||
|
||||
computed: {},
|
||||
|
||||
watch: {
|
||||
user () {
|
||||
this.updateUser()
|
||||
},
|
||||
show () {
|
||||
// Wait for modal to open and focus on first field
|
||||
setTimeout(() => {
|
||||
if (this.$refs.companyName) {
|
||||
this.$refs.companyName.$el.querySelector('input').focus()
|
||||
}
|
||||
}, 300)
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.updateUser()
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateUser () {
|
||||
if (this.user) {
|
||||
this.form.name = this.user.name
|
||||
this.form.email = this.user.email
|
||||
}
|
||||
},
|
||||
saveDetails () {
|
||||
if (this.form.busy) return
|
||||
this.form.put('api/subscription/update-customer-details').then(() => {
|
||||
this.loading = true
|
||||
axios.get('/api/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((response) => {
|
||||
window.location = response.data.checkout_url
|
||||
}).catch((error) => {
|
||||
this.alertError(error.response.data.message)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.close()
|
||||
})
|
||||
})
|
||||
},
|
||||
close () {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
41
client/components/pages/pricing/CustomPlan.vue
Normal file
41
client/components/pages/pricing/CustomPlan.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="border relative max-w-5xl mx-auto mt-4 lg:mt-10">
|
||||
<div class="w-full">
|
||||
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 px-6 py-8 sm:p-10 lg:flex lg:items-center">
|
||||
<div class="flex-1">
|
||||
<h3 class="inline-flex px-4 py-1 rounded-full text-md font-bold tracking-wide uppercase bg-white text-gray-800">
|
||||
Custom plan
|
||||
</h3>
|
||||
<div class="mt-4 text-md text-gray-600 dark:text-gray-400">
|
||||
Get a custom file upload limit, enterprise-level support, custom contract, dedicated application instance in a specific region, payment via invoice/PO etc.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 rounded-md lg:mt-0 lg:ml-10 lg:flex-shrink-0">
|
||||
<v-button color="white" class="w-full mt-4" @click.prevent="customPlanClick">
|
||||
Contact us
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomPlan',
|
||||
components: {},
|
||||
props: {},
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
computed: {},
|
||||
|
||||
methods: {
|
||||
customPlanClick () {
|
||||
window.$crisp.push(['do', 'chat:show'])
|
||||
window.$crisp.push(['do', 'chat:open'])
|
||||
window.$crisp.push(['do', 'message:send', ['text', 'Hi, I would like to discuss about a custom plan']])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
37
client/components/pages/pricing/MonthlyYearlySelector.vue
Normal file
37
client/components/pages/pricing/MonthlyYearlySelector.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="border border-gray-300 rounded-xl flex p-1 relative">
|
||||
<button class="font-semibold block flex-grow cursor-pointer">
|
||||
<div
|
||||
class="p-2 px-3 rounded-lg transition-colors"
|
||||
:class="{'bg-blue-500 text-white': !modelValue}"
|
||||
@click="set(false)"
|
||||
>
|
||||
Monthly
|
||||
</div>
|
||||
</button>
|
||||
<button class="font-semibold block flex-grow cursor-pointer" @click="set(true)">
|
||||
<div
|
||||
class="p-2 px-4 rounded-lg transition-colors"
|
||||
:class="{'bg-blue-500 text-white': modelValue}"
|
||||
>
|
||||
Yearly
|
||||
</div>
|
||||
</button>
|
||||
<div class="absolute hidden sm:block text-gray-500 text-xs mt-12">
|
||||
Save 20% with annual plans
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, defineProps } from 'vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: { type: Boolean, required: true }
|
||||
})
|
||||
const emit = defineEmits()
|
||||
|
||||
const set = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
173
client/components/pages/pricing/PricingTable.vue
Normal file
173
client/components/pages/pricing/PricingTable.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<section class="relative">
|
||||
<div v-if="!homePage" class="absolute inset-0 grid" aria-hidden="true">
|
||||
<div class="bg-gray-100" />
|
||||
<div class="bg-white" />
|
||||
</div>
|
||||
|
||||
<div class="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="max-w-5xl mx-auto bg-white shadow-xl rounded-3xl ring-1 ring-gray-200 lg:flex isolate">
|
||||
<div class="p-8 sm:p-8 lg:flex-auto">
|
||||
<h3 v-if="homePage" class="text-3xl font-semibold tracking-tight text-gray-950">
|
||||
Check out our
|
||||
<span class="ml-2 text-nt-blue">
|
||||
<svg class="inline w-10 h-10" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.12">
|
||||
<path d="M15.9998 27.3333L10.6665 12H21.3332L15.9998 27.3333Z" fill="currentColor" />
|
||||
<path d="M13.3332 4H9.33317L2.6665 12L10.6665 12L13.3332 4Z" fill="currentColor" />
|
||||
<path d="M18.6665 4H22.6665L29.3332 12L21.3332 12L18.6665 4Z" fill="currentColor" />
|
||||
</g>
|
||||
<path
|
||||
d="M3.33345 12H28.6668M13.3334 4L10.6668 12L16.0001 27.3333L21.3334 12L18.6668 4M16.8195 27.0167L28.7644 12.6829C28.9668 12.4399 29.0681 12.3185 29.1067 12.1829C29.1408 12.0633 29.1408 11.9367 29.1067 11.8171C29.0681 11.6815 28.9668 11.5601 28.7644 11.3171L22.9866 4.3838C22.8691 4.24273 22.8103 4.17219 22.7382 4.12148C22.6744 4.07654 22.6031 4.04318 22.5277 4.02289C22.4426 4 22.3508 4 22.1672 4H9.83305C9.64941 4 9.55758 4 9.4725 4.02289C9.39711 4.04318 9.32586 4.07654 9.26202 4.12148C9.18996 4.17219 9.13118 4.24273 9.01361 4.3838L3.23583 11.3171C3.0334 11.5601 2.93218 11.6815 2.8935 11.8171C2.85939 11.9366 2.85939 12.0633 2.8935 12.1829C2.93218 12.3185 3.0334 12.4399 3.23583 12.6829L15.1807 27.0167C15.4621 27.3544 15.6028 27.5232 15.7713 27.5848C15.919 27.6388 16.0812 27.6388 16.229 27.5848C16.3974 27.5232 16.5381 27.3544 16.8195 27.0167Z"
|
||||
stroke="currentColor" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Pro Features
|
||||
</span>
|
||||
</h3>
|
||||
<h3 v-else class="text-3xl font-semibold tracking-tight text-gray-950">
|
||||
Pro Plan
|
||||
</h3>
|
||||
<p class="mt-2 text-base font-medium leading-7 text-gray-600">
|
||||
OpnForm Pro offers empowering features tailored to the advanced needs of teams and creators. Enjoy our
|
||||
free 3-day trial!
|
||||
</p>
|
||||
|
||||
<div class="flex items-center mt-6 gap-x-4">
|
||||
<h4 class="flex-none text-sm font-semibold leading-6 tracking-widest text-gray-400 uppercase">
|
||||
What's included
|
||||
</h4>
|
||||
<div class="flex-auto h-px bg-gray-200" />
|
||||
</div>
|
||||
|
||||
<ul role="list"
|
||||
class="grid grid-cols-1 gap-4 mt-4 text-sm font-medium leading-6 text-gray-900 sm:grid-cols-2 sm:gap-x-6 sm:gap-y-2"
|
||||
>
|
||||
<li v-for="(title, i) in pricingInfo" :key="i" class="flex gap-x-3">
|
||||
<svg aria-hidden="true" class="w-5 h-5 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M20 6L9 17L4 12" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
{{ title }}
|
||||
</li>
|
||||
<slot name="pricing-table" />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="p-2 -mt-2 flex-col lg:mt-0 lg:w-full lg:max-w-md lg:flex-shrink-0">
|
||||
<div
|
||||
class="grow h-full py-10 text-center rounded-2xl bg-gray-50 ring-1 ring-inset ring-gray-900/5 lg:flex lg:flex-col lg:justify-center lg:py-12"
|
||||
>
|
||||
<div class="max-w-xs px-8 mx-auto space-y-6">
|
||||
<div class="flex items-center justify-center mb-10">
|
||||
<monthly-yearly-selector v-model="isYearly" />
|
||||
</div><!-- lg+ -->
|
||||
|
||||
<p class="flex flex-col items-center">
|
||||
<span class="text-6xl font-semibold tracking-tight text-gray-950">
|
||||
<template v-if="isYearly">$16</template>
|
||||
<template v-else>$19</template>
|
||||
</span>
|
||||
<span class="text-sm font-medium leading-6 text-gray-600">
|
||||
per month
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'register' }" :arrow="true">
|
||||
Start free trial
|
||||
</v-button>
|
||||
<v-button v-else-if="authenticated && user.is_subscribed" class="mr-1" :arrow="true"
|
||||
@click.prevent="openBilling"
|
||||
>
|
||||
View Billing
|
||||
</v-button>
|
||||
<v-button v-else class="mr-1" :arrow="true" @click.prevent="openCustomerCheckout('default')">
|
||||
Start free trial
|
||||
</v-button>
|
||||
</div>
|
||||
<p v-if="!homePage" class="text-xs font-medium leading-5 text-gray-600">
|
||||
Invoices and receipts available for easy company reimbursement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<custom-plan v-if="!homePage" />
|
||||
|
||||
<checkout-details-modal :show="showDetailsModal" :yearly="isYearly" :plan="selectedPlan"
|
||||
@close="showDetailsModal=false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
import axios from 'axios'
|
||||
import MonthlyYearlySelector from './MonthlyYearlySelector.vue'
|
||||
import CheckoutDetailsModal from './CheckoutDetailsModal.vue'
|
||||
import CustomPlan from './CustomPlan.vue'
|
||||
|
||||
MonthlyYearlySelector.compatConfig = { MODE: 3 }
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MonthlyYearlySelector,
|
||||
CheckoutDetailsModal,
|
||||
CustomPlan
|
||||
},
|
||||
props: {
|
||||
homePage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
authenticated : computed(() => authStore.check),
|
||||
user : computed(() => authStore.user)
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
isYearly: true,
|
||||
selectedPlan: 'pro',
|
||||
showDetailsModal: false,
|
||||
|
||||
pricingInfo: [
|
||||
'Form confirmation emails',
|
||||
'Slack notifications',
|
||||
'Discord notifications',
|
||||
'Editable submissions',
|
||||
'1 Custom domain',
|
||||
'Custom code',
|
||||
'Larger file uploads (50mb)',
|
||||
'Remove OpnForm branding',
|
||||
'Priority support'
|
||||
]
|
||||
}),
|
||||
|
||||
methods: {
|
||||
openCustomerCheckout (plan) {
|
||||
this.selectedPlan = plan
|
||||
this.showDetailsModal = true
|
||||
},
|
||||
openBilling () {
|
||||
this.billingLoading = true
|
||||
axios.get('/api/subscription/billing-portal').then((response) => {
|
||||
this.billingLoading = false
|
||||
const url = response.data.portal_url
|
||||
window.location = url
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
computed: {}
|
||||
}
|
||||
</script>
|
||||
88
client/components/pages/templates/SingleTemplate.vue
Normal file
88
client/components/pages/templates/SingleTemplate.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div v-if="template" class="relative group">
|
||||
<div v-if="template.is_new" class="absolute top-0 right-0 p-3 z-10">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-500 px-2 py-1 text-xs font-medium text-white"
|
||||
>
|
||||
<svg aria-hidden="true" class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path fill-rule="evenodd"
|
||||
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
New
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="aspect-[4/3] rounded-lg shadow-sm overflow-hidden">
|
||||
<img class="group-hover:scale-110 transition-all duration-200 h-full object-cover w-full"
|
||||
:src="template.image_url" alt=""
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
class="text-lg font-semibold leading-tight tracking-tight text-gray-900 mt-4 group-hover:text-blue-500 transition-all duration-150"
|
||||
>
|
||||
{{ template.name }}
|
||||
</p>
|
||||
<p class="line-clamp-2 mt-2 text-sm font-normal text-gray-600">
|
||||
{{ cleanQuotes(template.short_description) }}
|
||||
</p>
|
||||
<template-tags :slug="template.slug"
|
||||
class="flex mt-4 items-center flex-wrap gap-3"
|
||||
/>
|
||||
<router-link :to="{params:{slug:template.slug},name:'templates.show'}" title="">
|
||||
<span class="absolute inset-0" aria-hidden="true" />
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useTemplatesStore } from '../../../stores/templates'
|
||||
import TemplateTags from './TemplateTags.vue'
|
||||
|
||||
export default {
|
||||
components: { TemplateTags },
|
||||
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
return {
|
||||
templatesStore
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
template () {
|
||||
return this.templatesStore.getBySlug(this.slug)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
slug () {
|
||||
this.templatesStore.loadTemplate(this.slug)
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.templatesStore.loadTemplate(this.slug)
|
||||
},
|
||||
|
||||
methods: {
|
||||
cleanQuotes (str) {
|
||||
// Remove starting and ending quotes if any
|
||||
return (str) ? str.replace(/^"/, '').replace(/"$/, '') : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
client/components/pages/templates/TemplateTags.vue
Normal file
84
client/components/pages/templates/TemplateTags.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div v-if="template">
|
||||
<template v-if="displayAll">
|
||||
<span v-if="template.is_new"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-white bg-blue-500 rounded-full"
|
||||
>
|
||||
<svg aria-hidden="true" class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path fill-rule="evenodd"
|
||||
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
New
|
||||
</span>
|
||||
<span v-for="item in types"
|
||||
class="inline-flex items-center rounded-full bg-gray-50 dark:bg-gray-800 dark:text-gray-400 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
<span v-for="item in industries"
|
||||
class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-900 dark:text-gray-400 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="types.length > 0"
|
||||
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
|
||||
>
|
||||
{{ types[0] }} <template v-if="types.length > 1">+{{ types.length - 1 }}</template>
|
||||
</span>
|
||||
<span v-if="industries.length > 0"
|
||||
class="inline-flex items-center rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10"
|
||||
>
|
||||
{{ industries[0] }} <template v-if="industries.length > 1">+{{ industries.length - 1 }}</template>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useTemplatesStore } from '../../../stores/templates'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
displayAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
return {
|
||||
templatesStore
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
template () {
|
||||
return this.templatesStore.getBySlug(this.slug)
|
||||
},
|
||||
types () {
|
||||
if (!this.template) return null
|
||||
return this.templatesStore.getTemplateTypes(this.template.types)
|
||||
},
|
||||
industries () {
|
||||
if (!this.template) return null
|
||||
return this.templatesStore.getTemplateIndustries(this.template.industries)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
190
client/components/pages/templates/TemplatesList.vue
Normal file
190
client/components/pages/templates/TemplatesList.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="bg-white py-12">
|
||||
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sm:gap-6 relative z-20">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<select-input v-model="selectedType" name="type"
|
||||
:options="typesOptions" class="w-full sm:w-auto md:w-56"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<select-input v-model="selectedIndustry" name="industry"
|
||||
:options="industriesOptions" class="w-full sm:w-auto md:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 w-full md:max-w-xs">
|
||||
<text-input name="search" :form="searchTemplate" placeholder="Search..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="templatesLoading" class="text-center mt-4">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
<p v-else-if="enrichedTemplates.length === 0" class="text-center mt-4">
|
||||
No templates found.
|
||||
</p>
|
||||
<div v-else class="relative z-10">
|
||||
<div class="grid grid-cols-1 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 sm:gap-y-12">
|
||||
<single-template v-for="template in enrichedTemplates" :key="template.id" :slug="template.slug" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-if="!onlyMy">
|
||||
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
|
||||
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
|
||||
All Types
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<router-link v-for="row in types" :key="row.slug"
|
||||
:to="{params:{slug:row.slug}, name:'templates.types.show'}"
|
||||
:title="row.name"
|
||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||
>
|
||||
{{ row.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
|
||||
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
|
||||
All Industries
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<router-link v-for="row in industries" :key="row.slug"
|
||||
:to="{params:{slug:row.slug}, name:'templates.industries.show'}"
|
||||
:title="row.name"
|
||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||
>
|
||||
{{ row.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
import { useTemplatesStore } from '../../../stores/templates'
|
||||
import Form from 'vform'
|
||||
import Fuse from 'fuse.js'
|
||||
import SingleTemplate from './SingleTemplate.vue'
|
||||
|
||||
const loadTemplates = function (onlyMy) {
|
||||
const templatesStore = useTemplatesStore()
|
||||
if(onlyMy){
|
||||
templatesStore.loadAll({'onlymy':true})
|
||||
} else {
|
||||
templatesStore.loadIfEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'TemplatesList',
|
||||
components: { SingleTemplate },
|
||||
props: {
|
||||
onlyMy: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
const templatesStore = useTemplatesStore()
|
||||
return {
|
||||
user : computed(() => authStore.user),
|
||||
templates : computed(() => templatesStore.content),
|
||||
templatesLoading : computed(() => templatesStore.loading),
|
||||
industries : computed(() => templatesStore.industries),
|
||||
types : computed(() => templatesStore.types)
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
selectedType: 'all',
|
||||
selectedIndustry: 'all',
|
||||
searchTemplate: new Form({
|
||||
search: ''
|
||||
})
|
||||
}),
|
||||
|
||||
watch: {},
|
||||
|
||||
mounted () {
|
||||
loadTemplates(this.onlyMy)
|
||||
},
|
||||
|
||||
computed: {
|
||||
industriesOptions () {
|
||||
return [{ name: 'All Industries', value: 'all' }].concat(Object.values(this.industries).map((industry) => {
|
||||
return {
|
||||
name: industry.name,
|
||||
value: industry.slug
|
||||
}
|
||||
}))
|
||||
},
|
||||
typesOptions () {
|
||||
return [{ name: 'All Types', value: 'all' }].concat(Object.values(this.types).map((type) => {
|
||||
return {
|
||||
name: type.name,
|
||||
value: type.slug
|
||||
}
|
||||
}))
|
||||
},
|
||||
enrichedTemplates () {
|
||||
let enrichedTemplates = (this.onlyMy && this.user) ? this.templates.filter((item) => { return item.creator_id === this.user.id}) : this.templates
|
||||
|
||||
// Filter by Selected Type
|
||||
if (this.selectedType && this.selectedType !== 'all') {
|
||||
enrichedTemplates = enrichedTemplates.filter((item) => {
|
||||
return (item.types && item.types.length > 0) ? item.types.includes(this.selectedType) : false
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by Selected Industry
|
||||
if (this.selectedIndustry && this.selectedIndustry !== 'all') {
|
||||
enrichedTemplates = enrichedTemplates.filter((item) => {
|
||||
return (item.industries && item.industries.length > 0) ? item.industries.includes(this.selectedIndustry) : false
|
||||
})
|
||||
}
|
||||
|
||||
if (this.searchTemplate.search === '' || this.searchTemplate.search === null) {
|
||||
return enrichedTemplates
|
||||
}
|
||||
|
||||
// Fuze search
|
||||
const fuzeOptions = {
|
||||
keys: [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'short_description'
|
||||
]
|
||||
}
|
||||
const fuse = new Fuse(enrichedTemplates, fuzeOptions)
|
||||
return fuse.search(this.searchTemplate.search).map((res) => {
|
||||
return res.item
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
107
client/components/pages/welcome/AiFeature.vue
Normal file
107
client/components/pages/welcome/AiFeature.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="py-8">
|
||||
<section>
|
||||
<div class="mx-auto max-w-7xl isolate sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="relative px-4 pt-16 overflow-hidden bg-blue-100 ring-blue-100 ring-1 sm:shadow-lg isolate sm:rounded-2xl sm:px-16 md:pt-20 lg:flex lg:gap-x-20 lg:px-16 lg:pt-0 sm:shadow-gray-600/10">
|
||||
<div class="absolute inset-0">
|
||||
<img class="object-cover object-top w-full h-full" src="/img/pages/ai_form_builder/background-pattern-ai.svg" alt="">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-md mx-auto text-center xl:max-w-lg lg:mx-0 lg:flex-auto lg:py-16 lg:text-left">
|
||||
<span
|
||||
class="bg-white text-xs font-semibold inline-flex items-center shadow-sm ring-blue-200 ring-1 text-blue-600 px-2.5 py-1.5 rounded-full">
|
||||
<svg aria-hidden="true" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Introducing OpnForm AI
|
||||
</span>
|
||||
|
||||
<h2 class="mt-6 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl lg:text-4xl">
|
||||
Say goodbye to tedious form building with OpnForm's new <span
|
||||
class="text-transparent bg-clip-text bg-gradient-to-r lg:block from-blue-600 to-blue-300">AI-powered
|
||||
feature!</span>
|
||||
</h2>
|
||||
<p class="mt-4 text-base font-medium leading-7 text-gray-500 sm:text-lg sm:leading-8">
|
||||
Easily generate a fully working form in seconds with just a simple description.
|
||||
</p>
|
||||
|
||||
<ul
|
||||
class="flex flex-wrap items-center justify-center gap-4 mt-6 text-sm font-medium text-gray-900 lg:justify-start sm:mt-8">
|
||||
<li class="flex items-center gap-2">
|
||||
<svg aria-hidden="true" class="w-5 h-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Create form in minutes
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-2">
|
||||
<svg aria-hidden="true" class="w-5 h-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Customizations
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-2">
|
||||
<svg aria-hidden="true" class="w-5 h-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
No-coding required
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 sm:mt-8 flex text-center justify-center lg:justify-start">
|
||||
<!-- <v-button v-if="!authenticated" class="mr-2 block" :to="{ name: 'forms.create.guest' }" :arrow="true">-->
|
||||
<!-- Get started for free-->
|
||||
<!-- </v-button>-->
|
||||
<!-- <v-button v-else class="mr-2 block" :to="{ name: 'forms.create' }" :arrow="true">-->
|
||||
<!-- Get started for free-->
|
||||
<!-- </v-button>-->
|
||||
<!-- <v-button color="light-gray" class="mr-1 block" :to="{ name: 'aiformbuilder' }">-->
|
||||
<!-- Learn more-->
|
||||
<!-- </v-button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative px-6 mx-auto mt-8 lg:px-0 sm:mt-12 lg:bottom-0 lg:right-0 lg:absolute lg:max-w-md xl:max-w-none">
|
||||
<img class="rounded-t-2xl ring-1 ring-blue-100 lg:rounded-tr-none"
|
||||
src="/img/pages/ai_form_builder/ai-feature-illustration.svg" alt="App screenshot">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
return {
|
||||
authenticated : computed(() => authStore.check)
|
||||
}
|
||||
},
|
||||
props: {},
|
||||
data: () => ({}),
|
||||
computed: {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
130
client/components/pages/welcome/Features.vue
Normal file
130
client/components/pages/welcome/Features.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div id="features" class="px-4 mx-auto sm:max-w-xl md:max-w-full lg:max-w-screen-xl md:px-24 lg:px-8">
|
||||
|
||||
<div class="mb-16 max-w-xl md:mx-auto sm:text-center lg:max-w-2xl ">
|
||||
<h2
|
||||
class="mb-6 font-sans text-4xl font-semibold leading-none tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl md:mx-auto">
|
||||
The easiest way to create forms. <br/>
|
||||
Generous unlimited <span class="text-nt-blue">free plan.</span>
|
||||
</h2>
|
||||
<p class="text-base text-gray-700 dark:text-gray-300 md:text-lg">
|
||||
Need a contact form? Doing a survey? Create a form in 2 minutes and start receiving submissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center mt-16" :class="{'md:flex-row-reverse':index%2==1}" v-for="(step,index) in [
|
||||
{
|
||||
title: 'Create',
|
||||
description: 'Create a form in 2 minutes. More than 10 input types, images, logic and much more.',
|
||||
features: [
|
||||
'Build a simple form in minutes.',
|
||||
'No coding needed.'
|
||||
],
|
||||
img: '/img/pages/welcome/step-1.jpg'
|
||||
},
|
||||
{
|
||||
title: 'Share',
|
||||
description: 'Your form has a unique link that you can share everywhere. Send the link, or even embed the form on your website.',
|
||||
features: [
|
||||
'Share the link to your form',
|
||||
'Embed the form on your website'
|
||||
],
|
||||
img: '/img/pages/welcome/step-2.jpg'
|
||||
},
|
||||
{
|
||||
title: 'Get Results',
|
||||
description: 'Receive your form submissions. Receive notifications, send confirmations. Export submissions and check your form analytics.',
|
||||
features: [
|
||||
'Unlimited form submissions for free',
|
||||
'Easily export submissions as CSV',
|
||||
'Views & Submissions Analytics'
|
||||
],
|
||||
img: '/img/pages/welcome/step-3.jpg'
|
||||
}
|
||||
]" :key="step.title">
|
||||
<div class="w-full md:w-1/2 lg:w-5/12" :class="{'md:pl-4':index%2==1, 'md:pr-4':index%2==0}">
|
||||
<svg v-if="step.title=='Create'" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor" class="w-10 h-10 text-nt-blue">
|
||||
<path
|
||||
d="M22 7.99997H13.6C10.2397 7.99997 8.55953 7.99997 7.27606 8.65393C6.14708 9.22917 5.2292 10.1471 4.65396 11.276C4 12.5595 4 14.2397 4 17.6V34.4C4 37.7603 4 39.4404 4.65396 40.7239C5.2292 41.8529 6.14708 42.7708 7.27606 43.346C8.55953 44 10.2397 44 13.6 44H30.4C33.7603 44 35.4405 44 36.7239 43.346C37.8529 42.7708 38.7708 41.8529 39.346 40.7239C40 39.4404 40 37.7603 40 34.4V26M15.9999 32H19.349C20.3274 32 20.8166 32 21.2769 31.8894C21.6851 31.7915 22.0753 31.6298 22.4331 31.4105C22.8368 31.1632 23.1827 30.8173 23.8745 30.1255L43 11C44.6569 9.34311 44.6569 6.65682 43 4.99997C41.3431 3.34311 38.6569 3.34311 37 4.99996L17.8745 24.1255C17.1827 24.8173 16.8368 25.1632 16.5894 25.5668C16.3701 25.9247 16.2085 26.3149 16.1105 26.723C15.9999 27.1834 15.9999 27.6726 15.9999 28.6509V32Z"
|
||||
stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg v-else-if="step.title=='Share'" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor" class="w-10 h-10 text-nt-blue">
|
||||
<path
|
||||
d="M17.18 27.02L30.84 34.98M30.82 13.02L17.18 20.98M42 10C42 13.3137 39.3137 16 36 16C32.6863 16 30 13.3137 30 10C30 6.68629 32.6863 4 36 4C39.3137 4 42 6.68629 42 10ZM18 24C18 27.3137 15.3137 30 12 30C8.68629 30 6 27.3137 6 24C6 20.6863 8.68629 18 12 18C15.3137 18 18 20.6863 18 24ZM42 38C42 41.3137 39.3137 44 36 44C32.6863 44 30 41.3137 30 38C30 34.6863 32.6863 32 36 32C39.3137 32 42 34.6863 42 38Z"
|
||||
stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg v-else-if="step.title=='Share'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-nt-blue">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>
|
||||
</svg>
|
||||
<svg v-else-if="step.title=='Get Results'" class="w-10 h-10 text-nt-blue" viewBox="0 0 48 48" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M42 42H9.2C8.07989 42 7.51984 42 7.09202 41.782C6.71569 41.5903 6.40973 41.2843 6.21799 40.908C6 40.4802 6 39.9201 6 38.8V6M40 16L32.1623 24.3653C31.8652 24.6823 31.7167 24.8409 31.5375 24.9228C31.3794 24.9951 31.2051 25.0249 31.0319 25.0093C30.8357 24.9916 30.6429 24.8915 30.2574 24.6913L23.7426 21.3087C23.3571 21.1085 23.1643 21.0084 22.9681 20.9907C22.7949 20.9751 22.6206 21.0049 22.4625 21.0772C22.2833 21.1591 22.1348 21.3177 21.8377 21.6347L14 30"
|
||||
stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
||||
<h4 class="my-5 text-2xl font-medium">{{ index + 1 }}. {{ step.title }}</h4>
|
||||
<p class="dark:text-white">
|
||||
{{ step.description }}
|
||||
</p>
|
||||
<div class="mb-8">
|
||||
<div class="flex mt-4" v-for="feature in step.features" :key="feature">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-4 h-4 mt-1 mr-2 text-nt-blue">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
|
||||
</svg>
|
||||
{{ feature }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 lg:w-7/12 flex items-center justify-center relative w-full"
|
||||
:class="{'md:pr-8':index%2==1, 'md:pl-8':index%2==0}">
|
||||
<img loading="lazy" class="block rounded-2xl w-full"
|
||||
:src="step.img" alt="cover-product">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 mt-20">
|
||||
<div class="mb-8 md:mr-10">
|
||||
<svg class="w-10 h-10 text-nt-blue" viewBox="0 0 44 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.0006 40H18.0006M2.58828 9.63978C2.55958 6.73709 4.12455 4.02649 6.6527 2.59999M41.405 9.6398C41.4337 6.7371 39.8687 4.0265 37.3406 2.60001M34.0006 14C34.0006 10.8174 32.7363 7.76516 30.4859 5.51472C28.2354 3.26428 25.1832 2 22.0006 2C18.818 2 15.7658 3.26428 13.5153 5.51472C11.2649 7.76516 10.0006 10.8174 10.0006 14C10.0006 20.1804 8.44154 24.4119 6.69993 27.2108C5.23085 29.5717 4.49631 30.7522 4.52325 31.0815C4.55307 31.4461 4.63032 31.5852 4.92415 31.8032C5.18951 32 6.38578 32 8.7783 32H35.2229C37.6154 32 38.8117 32 39.077 31.8032C39.3709 31.5852 39.4481 31.4461 39.4779 31.0815C39.5049 30.7522 38.7703 29.5718 37.3013 27.2108C35.5597 24.4119 34.0006 20.1804 34.0006 14Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h3 class="my-3 font-semibold">Notifications</h3>
|
||||
<p>Receive notifications directly in Slack or in your mailbox whenever your from has a new submission (if you
|
||||
want to).</p>
|
||||
</div>
|
||||
<div class="mb-8 md:mr-10">
|
||||
<svg class="w-10 h-10 text-nt-blue" viewBox="0 0 45 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6666 2H27.0666C30.4269 2 32.1071 2 33.3906 2.65396C34.5195 3.2292 35.4374 4.14708 36.0127 5.27606C36.6666 6.55953 36.6666 8.23969 36.6666 11.6V32.4C36.6666 35.7603 36.6666 37.4405 36.0127 38.7239C35.4374 39.8529 34.5195 40.7708 33.3906 41.346C32.1071 42 30.4269 42 27.0666 42H14.2666C10.9063 42 9.22615 42 7.94268 41.346C6.81371 40.7708 5.89583 39.8529 5.32059 38.7239C4.66663 37.4405 4.66663 35.7603 4.66663 32.4V31M28.6666 24H19.6666M28.6666 16H21.6666M28.6666 32H12.6666M8.66663 18V7C8.66663 5.34315 10.0098 4 11.6666 4C13.3235 4 14.6666 5.34315 14.6666 7V18C14.6666 21.3137 11.9803 24 8.66663 24C5.35292 24 2.66663 21.3137 2.66663 18V10" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h3 class="my-3 font-semibold">File Uploads</h3>
|
||||
<p>Easily add file upload inputs to your forms. Uploaded files are securely stored for you. Up to 5mb!</p>
|
||||
</div>
|
||||
<div class="mb-8 md:mr-10">
|
||||
<svg class="w-10 h-10 text-nt-blue" viewBox="0 0 45 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.33331 22C2.33331 33.0457 11.2876 42 22.3333 42C25.647 42 28.3333 39.3137 28.3333 36V35C28.3333 34.0712 28.3333 33.6067 28.3846 33.2168C28.7391 30.5244 30.8578 28.4058 33.5502 28.0513C33.9401 28 34.4045 28 35.3333 28H36.3333C39.647 28 42.3333 25.3137 42.3333 22C42.3333 10.9543 33.379 2 22.3333 2C11.2876 2 2.33331 10.9543 2.33331 22Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.3333 24C13.4379 24 14.3333 23.1046 14.3333 22C14.3333 20.8954 13.4379 20 12.3333 20C11.2287 20 10.3333 20.8954 10.3333 22C10.3333 23.1046 11.2287 24 12.3333 24Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M30.3333 16C31.4379 16 32.3333 15.1046 32.3333 14C32.3333 12.8954 31.4379 12 30.3333 12C29.2287 12 28.3333 12.8954 28.3333 14C28.3333 15.1046 29.2287 16 30.3333 16Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.3333 14C19.4379 14 20.3333 13.1046 20.3333 12C20.3333 10.8954 19.4379 10 18.3333 10C17.2287 10 16.3333 10.8954 16.3333 12C16.3333 13.1046 17.2287 14 18.3333 14Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h3 class="my-3 font-semibold">Customize Everything</h3>
|
||||
<p>Change form themes, change texts, colors, add images, add custom thank you pages and much more.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
props: {},
|
||||
data: () => ({}),
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
84
client/components/pages/welcome/MoreFeatures.vue
Normal file
84
client/components/pages/welcome/MoreFeatures.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 dark:bg-notion-dark py-8">
|
||||
<div class="md:max-w-5xl md:mx-auto w-full">
|
||||
<div class="my-5 text-center">
|
||||
<h3 class="font-semibold text-3xl">And many more features</h3>
|
||||
<p class="w-full mt-2 mb-8">
|
||||
OpnForm makes form building easy and comes with powerful features.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 mt-10 mb-5 ml-5 md:ml-0 px-4">
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 3.5H17.8C18.9201 3.5 19.4802 3.5 19.908 3.71799C20.2843 3.90973 20.5903 4.21569 20.782 4.59202C21 5.01984 21 5.57989 21 6.7V8C21 8.93188 21 9.39783 20.8478 9.76537C20.6448 10.2554 20.2554 10.6448 19.7654 10.8478C19.3978 11 18.9319 11 18 11M12.5 18.5H5.2C4.0799 18.5 3.51984 18.5 3.09202 18.282C2.71569 18.0903 2.40973 17.7843 2.21799 17.408C2 16.9802 2 16.4201 2 15.3V14C2 13.0681 2 12.6022 2.15224 12.2346C2.35523 11.7446 2.74458 11.3552 3.23463 11.1522C3.60218 11 4.06812 11 5 11M9.8 13.5H13.2C13.48 13.5 13.62 13.5 13.727 13.4455C13.8211 13.3976 13.8976 13.3211 13.9455 13.227C14 13.12 14 12.98 14 12.7V9.3C14 9.01997 14 8.87996 13.9455 8.773C13.8976 8.67892 13.8211 8.60243 13.727 8.5545C13.62 8.5 13.48 8.5 13.2 8.5H9.8C9.51997 8.5 9.37996 8.5 9.273 8.5545C9.17892 8.60243 9.10243 8.67892 9.0545 8.773C9 8.87996 9 9.01997 9 9.3V12.7C9 12.98 9 13.12 9.0545 13.227C9.10243 13.3211 9.17892 13.3976 9.273 13.4455C9.37996 13.5 9.51997 13.5 9.8 13.5ZM17.3 21H20.7C20.98 21 21.12 21 21.227 20.9455C21.3211 20.8976 21.3976 20.8211 21.4455 20.727C21.5 20.62 21.5 20.48 21.5 20.2V16.8C21.5 16.52 21.5 16.38 21.4455 16.273C21.3976 16.1789 21.3211 16.1024 21.227 16.0545C21.12 16 20.98 16 20.7 16H17.3C17.02 16 16.88 16 16.773 16.0545C16.6789 16.1024 16.6024 16.1789 16.5545 16.273C16.5 16.38 16.5 16.52 16.5 16.8V20.2C16.5 20.48 16.5 20.62 16.5545 20.727C16.6024 20.8211 16.6789 20.8976 16.773 20.9455C16.88 21 17.02 21 17.3 21ZM2.3 6H5.7C5.98003 6 6.12004 6 6.227 5.9455C6.32108 5.89757 6.39757 5.82108 6.4455 5.727C6.5 5.62004 6.5 5.48003 6.5 5.2V1.8C6.5 1.51997 6.5 1.37996 6.4455 1.273C6.39757 1.17892 6.32108 1.10243 6.227 1.0545C6.12004 1 5.98003 1 5.7 1H2.3C2.01997 1 1.87996 1 1.773 1.0545C1.67892 1.10243 1.60243 1.17892 1.5545 1.273C1.5 1.37996 1.5 1.51997 1.5 1.8V5.2C1.5 5.48003 1.5 5.62004 1.5545 5.727C1.60243 5.82108 1.67892 5.89757 1.773 5.9455C1.87996 6 2.01997 6 2.3 6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Form logic
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 11H21.5M1.5 11C1.5 16.5228 5.97715 21 11.5 21M1.5 11C1.5 5.47715 5.97715 1 11.5 1M21.5 11C21.5 16.5228 17.0228 21 11.5 21M21.5 11C21.5 5.47715 17.0228 1 11.5 1M11.5 1C14.0013 3.73835 15.4228 7.29203 15.5 11C15.4228 14.708 14.0013 18.2616 11.5 21M11.5 1C8.99872 3.73835 7.57725 7.29203 7.5 11C7.57725 14.708 8.99872 18.2616 11.5 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
URL pre-fill
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99999 1L4.99999 19M16 1L13 19M19 6H2M18 14H1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Unique submission ID
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.2429 3.09232C10.6494 3.03223 11.0686 3 11.5004 3C16.6054 3 19.9553 7.50484 21.0807 9.28682C21.2169 9.5025 21.285 9.61034 21.3231 9.77667C21.3518 9.90159 21.3517 10.0987 21.3231 10.2236C21.2849 10.3899 21.2164 10.4985 21.0792 10.7156C20.7793 11.1901 20.3222 11.8571 19.7165 12.5805M6.22432 4.71504C4.06225 6.1817 2.59445 8.21938 1.92111 9.28528C1.78428 9.50187 1.71587 9.61016 1.67774 9.77648C1.6491 9.9014 1.64909 10.0984 1.67771 10.2234C1.71583 10.3897 1.78393 10.4975 1.92013 10.7132C3.04554 12.4952 6.39541 17 11.5004 17C13.5588 17 15.3319 16.2676 16.7888 15.2766M2.50042 1L20.5004 19M9.3791 7.87868C8.8362 8.42157 8.50042 9.17157 8.50042 10C8.50042 11.6569 9.84356 13 11.5004 13C12.3288 13 13.0788 12.6642 13.6217 12.1213" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Hidden fields
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5 8V6C14.5 3.23858 12.2614 1 9.5 1C6.73858 1 4.5 3.23858 4.5 6V8M9.5 12.5V14.5M6.3 19H12.7C14.3802 19 15.2202 19 15.862 18.673C16.4265 18.3854 16.8854 17.9265 17.173 17.362C17.5 16.7202 17.5 15.8802 17.5 14.2V12.8C17.5 11.1198 17.5 10.2798 17.173 9.63803C16.8854 9.07354 16.4265 8.6146 15.862 8.32698C15.2202 8 14.3802 8 12.7 8H6.3C4.61984 8 3.77976 8 3.13803 8.32698C2.57354 8.6146 2.1146 9.07354 1.82698 9.63803C1.5 10.2798 1.5 11.1198 1.5 12.8V14.2C1.5 15.8802 1.5 16.7202 1.82698 17.362C2.1146 17.9265 2.57354 18.3854 3.13803 18.673C3.77976 19 4.61984 19 6.3 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Form password
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5 13C14.8431 13 13.5 14.3431 13.5 16C13.5 17.6569 14.8431 19 16.5 19C18.1569 19 19.5 17.6569 19.5 16C19.5 14.3431 18.1569 13 16.5 13ZM16.5 13V6C16.5 5.46957 16.2893 4.96086 15.9142 4.58579C15.5391 4.21071 15.0304 4 14.5 4H11.5M4.5 7C6.15685 7 7.5 5.65685 7.5 4C7.5 2.34315 6.15685 1 4.5 1C2.84315 1 1.5 2.34315 1.5 4C1.5 5.65685 2.84315 7 4.5 7ZM4.5 7V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Webhooks
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 13L16 10L13 7M8 7L5 10L8 13M6.3 19H14.7C16.3802 19 17.2202 19 17.862 18.673C18.4265 18.3854 18.8854 17.9265 19.173 17.362C19.5 16.7202 19.5 15.8802 19.5 14.2V5.8C19.5 4.11984 19.5 3.27976 19.173 2.63803C18.8854 2.07354 18.4265 1.6146 17.862 1.32698C17.2202 1 16.3802 1 14.7 1H6.3C4.61984 1 3.77976 1 3.13803 1.32698C2.57354 1.6146 2.1146 2.07354 1.82698 2.63803C1.5 3.27976 1.5 4.11984 1.5 5.8V14.2C1.5 15.8802 1.5 16.7202 1.82698 17.362C2.1146 17.9265 2.57354 18.3854 3.13803 18.673C3.77976 19 4.61984 19 6.3 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Custom code
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.5 7H1.5M14.5 1V4M6.5 1V4M10.5 17V11M7.5 14H13.5M6.3 21H14.7C16.3802 21 17.2202 21 17.862 20.673C18.4265 20.3854 18.8854 19.9265 19.173 19.362C19.5 18.7202 19.5 17.8802 19.5 16.2V7.8C19.5 6.11984 19.5 5.27976 19.173 4.63803C18.8854 4.07354 18.4265 3.6146 17.862 3.32698C17.2202 3 16.3802 3 14.7 3H6.3C4.61984 3 3.77976 3 3.13803 3.32698C2.57354 3.6146 2.1146 4.07354 1.82698 4.63803C1.5 5.27976 1.5 6.11984 1.5 7.8V16.2C1.5 17.8802 1.5 18.7202 1.82698 19.362C2.1146 19.9265 2.57354 20.3854 3.13803 20.673C3.77976 21 4.61984 21 6.3 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Closing date
|
||||
</div>
|
||||
<div class="flex font-semibold my-3">
|
||||
<svg class="w-5 h-5 mr-2 text-nt-blue" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 3C10.0523 3 10.5 2.55228 10.5 2C10.5 1.44772 10.0523 1 9.5 1C8.94772 1 8.5 1.44772 8.5 2C8.5 2.55228 8.94772 3 9.5 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 10C10.0523 10 10.5 9.55228 10.5 9C10.5 8.44772 10.0523 8 9.5 8C8.94772 8 8.5 8.44772 8.5 9C8.5 9.55228 8.94772 10 9.5 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 17C10.0523 17 10.5 16.5523 10.5 16C10.5 15.4477 10.0523 15 9.5 15C8.94772 15 8.5 15.4477 8.5 16C8.5 16.5523 8.94772 17 9.5 17Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.5 3C17.0523 3 17.5 2.55228 17.5 2C17.5 1.44772 17.0523 1 16.5 1C15.9477 1 15.5 1.44772 15.5 2C15.5 2.55228 15.9477 3 16.5 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.5 10C17.0523 10 17.5 9.55228 17.5 9C17.5 8.44772 17.0523 8 16.5 8C15.9477 8 15.5 8.44772 15.5 9C15.5 9.55228 15.9477 10 16.5 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.5 17C17.0523 17 17.5 16.5523 17.5 16C17.5 15.4477 17.0523 15 16.5 15C15.9477 15 15.5 15.4477 15.5 16C15.5 16.5523 15.9477 17 16.5 17Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 3C3.05228 3 3.5 2.55228 3.5 2C3.5 1.44772 3.05228 1 2.5 1C1.94772 1 1.5 1.44772 1.5 2C1.5 2.55228 1.94772 3 2.5 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 10C3.05228 10 3.5 9.55228 3.5 9C3.5 8.44772 3.05228 8 2.5 8C1.94772 8 1.5 8.44772 1.5 9C1.5 9.55228 1.94772 10 2.5 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 17C3.05228 17 3.5 16.5523 3.5 16C3.5 15.4477 3.05228 15 2.5 15C1.94772 15 1.5 15.4477 1.5 16C1.5 16.5523 1.94772 17 2.5 17Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
And much more...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {},
|
||||
data: () => ({}),
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
86
client/components/pages/welcome/TemplatesSlider.vue
Normal file
86
client/components/pages/welcome/TemplatesSlider.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="mx-auto mb-12 max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center">
|
||||
<h2 class="text-lg font-semibold leading-8 tracking-tight text-blue-500 ">
|
||||
Single or multi-page forms
|
||||
</h2>
|
||||
<p class="mt-2 text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Discover our beautiful templates
|
||||
</p>
|
||||
<p class="mt-3 px-8 text-center text-lg text-gray-400 ">
|
||||
If you need inspiration, checkout our templates.
|
||||
</p>
|
||||
</div>
|
||||
<div class="my-3 flex justify-center">
|
||||
<router-link :to="{name:'templates'}">
|
||||
See all templates
|
||||
<svg class="h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="templates.length > 0"
|
||||
class="w-full inline-flex flex-nowrap overflow-hidden [mask-image:_linear-gradient(to_right,transparent_0,_black_128px,_black_calc(100%-128px),transparent_100%)]"
|
||||
>
|
||||
<ul ref="templates-slider" class="flex justify-center md:justify-start animate-infinite-scroll">
|
||||
<li v-for="(template, i) in sliderTemplates" :key="template.id" class="mx-4 w-72 h-auto">
|
||||
<single-template :slug="template.slug" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useTemplatesStore } from '../../../stores/templates'
|
||||
import SingleTemplate from '../templates/SingleTemplate.vue'
|
||||
|
||||
export default {
|
||||
components: { SingleTemplate },
|
||||
props: { },
|
||||
setup () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
return {
|
||||
templatesStore,
|
||||
templates : computed(() => templatesStore.content)
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
sliderTemplates () {
|
||||
return this.templates.slice(0, 20)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
templates: {
|
||||
deep: true,
|
||||
handler () {
|
||||
this.$nextTick(() => {
|
||||
this.setInfinite()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.templatesStore.loadAll({ limit: 20 })
|
||||
},
|
||||
|
||||
methods: {
|
||||
setInfinite () {
|
||||
const ul = this.$refs['templates-slider']
|
||||
if (ul) {
|
||||
ul.insertAdjacentHTML('afterend', ul.outerHTML)
|
||||
ul.nextSibling.setAttribute('aria-hidden', 'true')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
46
client/components/pages/welcome/Testimonials.vue
Normal file
46
client/components/pages/welcome/Testimonials.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<iframe v-if="!isDarkMode" id="testimonialto-carousel-all-notionforms"
|
||||
loading="lazy"
|
||||
src="https://embed.testimonial.to/carousel/all/notionforms?theme=light&autoplay=on&showmore=on&one-row=on&same-height=off"
|
||||
frameBorder="0" scrolling="no" width="100%"
|
||||
/>
|
||||
<iframe v-else id="testimonialto-carousel-all-notionforms" src="https://embed.testimonial.to/carousel/all/notionforms?theme=dark&autoplay=on&showmore=on&one-row=on&same-height=off" frameborder="0" scrolling="no" width="100%" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
props: {
|
||||
featuresOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
isDarkMode () {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadScript()
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadScript () {
|
||||
if (process.server) return
|
||||
const script = document.createElement('script')
|
||||
script.setAttribute('src', 'https://testimonial.to/js/iframeResizer.min.js')
|
||||
document.head.appendChild(script)
|
||||
script.addEventListener('load', function () {
|
||||
window.iFrameResize({
|
||||
log: false,
|
||||
checkOrigin: false
|
||||
}, '#testimonialto-carousel-all-notionforms')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user