Improve Templates (#183)

* Improve Templates

* Fix test case

* Update AI GenerateTemplate

* update openai client and GPT completer

* composer.lock

* Update types and list json with script

* Template changes

* fix on draft template

* Finish opnform templates

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>
Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2023-09-08 16:30:28 +05:30
committed by GitHub
parent d93eca7410
commit 8e47b49e9a
36 changed files with 3130 additions and 1381 deletions

View File

@@ -1,22 +1,61 @@
<template>
<div class="breadcrumbs flex">
<div v-for="(item,index) in path" :key="item.label" class="flex items-center">
<router-link v-if="item.route" class="p-1 hover:bg-blue-50 rounded-md" :to="item.route">
{{ item.label }}
</router-link>
<div v-else class="p-1" :class="{'font-semibold': index===path.length-1}">
{{ item.label }}
</div>
<div v-if="index!==path.length-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<section class="sticky flex items-center inset-x-0 top-0 z-20 py-3 bg-white border-b border-gray-200">
<div class="hidden md:flex flex-grow">
<slot name="left" />
</div>
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="flex items-center justify-center space-x-4">
<div v-if="displayHome" class="flex items-center">
<router-link class="text-gray-400 hover:text-gray-500" :to="{ name: (authenticated) ? 'home' : 'welcome' }">
<svg class="flex-shrink-0 w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd"
d="M9.293 2.293a1 1 0 011.414 0l7 7A1 1 0 0117 11h-1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-6H3a1 1 0 01-.707-1.707l7-7z"
clip-rule="evenodd"
/>
</svg>
<span class="sr-only">Home</span>
</router-link>
<svg class="flex-shrink-0 w-5 h-5 text-gray-400 ml-4" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true"
>
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</div>
<div v-for="(item,index) in path" :key="index" class="flex items-center">
<router-link v-if="item.route" class="text-sm font-semibold text-gray-500 hover:text-gray-700 truncate"
:to="item.route"
>
{{ item.label }}
</router-link>
<div v-else class="text-sm font-semibold sm:w-full w-36 text-blue-500 truncate">
{{ item.label }}
</div>
<div v-if="index!==path.length-1">
<svg class="flex-shrink-0 w-5 h-5 text-gray-400 ml-4" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true"
>
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class="hidden md:flex flex-grow justify-end">
<slot name="right" />
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Breadcrumb',
props: {
@@ -28,10 +67,16 @@ export default {
},
data () {
return {}
return {
displayHome: true
}
},
computed: {},
computed: {
...mapGetters({
authenticated: 'auth/check'
})
},
mounted () {
},

View File

@@ -0,0 +1,182 @@
<template>
<modal :show="show" @close="$emit('close')">
<template #icon>
<svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17 27C16.0681 27 15.6022 27 15.2346 26.8478C14.7446 26.6448 14.3552 26.2554 14.1522 25.7654C14 25.3978 14 24.9319 14 24V17.2C14 16.0799 14 15.5198 14.218 15.092C14.4097 14.7157 14.7157 14.4097 15.092 14.218C15.5198 14 16.0799 14 17.2 14H24C24.9319 14 25.3978 14 25.7654 14.1522C26.2554 14.3552 26.6448 14.7446 26.8478 15.2346C27 15.6022 27 16.0681 27 17M24.2 34H30.8C31.9201 34 32.4802 34 32.908 33.782C33.2843 33.5903 33.5903 33.2843 33.782 32.908C34 32.4802 34 31.9201 34 30.8V24.2C34 23.0799 34 22.5198 33.782 22.092C33.5903 21.7157 33.2843 21.4097 32.908 21.218C32.4802 21 31.9201 21 30.8 21H24.2C23.0799 21 22.5198 21 22.092 21.218C21.7157 21.4097 21.4097 21.7157 21.218 22.092C21 22.5198 21 23.0799 21 24.2V30.8C21 31.9201 21 32.4802 21.218 32.908C21.4097 33.2843 21.7157 33.5903 22.092 33.782C22.5198 34 23.0799 34 24.2 34Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
</template>
<template #title>
<template v-if="template">
Edit Template
</template>
<template v-else>
Create Template
</template>
</template>
<div class="p-4">
<p v-if="!template">
New template will be create from your form <span class="font-semibold">{{ form.title }}</span>.
</p>
<form v-if="templateForm" class="mt-6" @submit.prevent="onSubmit" @keydown="templateForm.onKeydown($event)">
<div class="-m-6">
<div class="border-t py-4 px-6">
<toggle-switch-input name="publicly_listed" :form="templateForm" class="mt-4" label="Publicly Listed?" />
<text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" />
<text-input name="slug" :form="templateForm" class="mt-4" label="Slug" :required="true" />
<text-area-input name="short_description" :form="templateForm" class="mt-4" label="Short Description"
:required="true"
/>
<rich-text-area-input name="description" :form="templateForm" class="mt-4" label="Description"
:required="true"
/>
<text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" />
<select-input name="types" :form="templateForm" class="mt-4" label="Types" :options="typesOptions"
:multiple="true" :searchable="true"
/>
<select-input name="industries" :form="templateForm" class="mt-4" label="Industries"
:options="industriesOptions" :multiple="true" :searchable="true"
/>
<select-input name="related_templates" :form="templateForm" class="mt-4" label="Related Templates"
:options="templatesOptions" :multiple="true" :searchable="true"
/>
<questions-editor name="questions" :questions="templateForm.questions" class="mt-4"
label="Frequently asked questions"
/>
</div>
<div class="flex justify-end mt-4 pb-5 px-6">
<v-button class="mr-2" :loading="templateForm.busy">
<template v-if="template">
Update
</template>
<template v-else>
Create
</template>
</v-button>
<v-button v-if="template" color="red" class="mr-2"
@click.prevent="alertConfirm('Do you really want to delete this template?', deleteFormTemplate)"
>
Delete
</v-button>
<v-button color="white" @click.prevent="$emit('close')">
Close
</v-button>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
import Form from 'vform'
import store from '~/store'
import { mapState } from 'vuex'
import axios from 'axios'
import QuestionsEditor from './QuestionsEditor.vue'
export default {
name: 'FormTemplateModal',
components: { QuestionsEditor },
props: {
show: { type: Boolean, required: true },
form: { type: Object, required: true },
template: { type: Object, required: false, default: () => {} }
},
data: () => ({
templateForm: null
}),
mounted () {
this.templateForm = new Form(this.template ?? {
publicly_listed: true,
name: '',
slug: '',
short_description: '',
description: '',
image_url: '',
types: null,
industries: null,
related_templates: null,
questions: []
})
store.dispatch('open/templates/loadIfEmpty')
},
computed: {
...mapState({
templates: state => state['open/templates'].content,
industries: state => state['open/templates'].industries,
types: state => state['open/templates'].types
}),
typesOptions () {
return Object.values(this.types).map((type) => {
return {
name: type.name,
value: type.slug
}
})
},
industriesOptions () {
return Object.values(this.industries).map((industry) => {
return {
name: industry.name,
value: industry.slug
}
})
},
templatesOptions () {
return this.templates.map((template) => {
return {
name: template.name,
value: template.slug
}
})
}
},
methods: {
onSubmit () {
if (this.template) {
this.updateFormTemplate()
} else {
this.createFormTemplate()
}
},
async createFormTemplate () {
this.templateForm.form = this.form
await this.templateForm.post('/api/templates').then((response) => {
if (response.data.message) {
this.alertSuccess(response.data.message)
}
this.$emit('close')
})
},
async updateFormTemplate () {
this.templateForm.form = this.form
await this.templateForm.put('/api/templates/' + this.template.id).then((response) => {
if (response.data.message) {
this.alertSuccess(response.data.message)
}
this.$store.dispatch('open/templates/addOrUpdate', response.data.data)
this.$emit('close')
})
},
async deleteFormTemplate () {
if (!this.template) return
axios.delete('/api/templates/' + this.template.id).then((response) => {
if (response.data.message) {
this.alertSuccess(response.data.message)
}
this.$router.push({ name: 'templates' })
this.$store.dispatch('open/templates/remove', response.data.data)
this.$emit('close')
})
}
}
}
</script>

View File

@@ -0,0 +1,77 @@
<template>
<div :class="wrapperClass">
<label v-if="label" :for="id?id:name"
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
>
{{ label }}
<span v-if="required" class="text-red-500 required-dot">*</span>
</label>
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
<div v-else class="my-3">
<div v-for="(questionForm, quesKey) in allQuestions" :key="quesKey" class="bg-gray-100 p-2 mb-4">
<v-button color="red" size="small" class="mb-2" @click.prevent="onRemove(quesKey)">
<svg class="h-4 w-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Remove
</v-button>
<text-input name="question" :form="questionForm" placeholder="Question title" />
<rich-text-area-input name="answer" :form="questionForm" class="mt-4" placeholder="Question response" />
</div>
<v-button v-if="addNew" color="green" size="small" nativeType="button" class="mt-2 flex" @click.prevent="onAdd">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Add New
</v-button>
</div>
<small v-if="help" :class="theme.SelectInput.help">
<slot name="help">{{ help }}</slot>
</small>
<has-error v-if="hasValidation" :form="form" :field="name" />
</div>
</template>
<script>
import inputMixin from '~/mixins/forms/input.js'
export default {
name: 'QuestionsEditor',
mixins: [inputMixin],
props: {
loading: { type: Boolean, default: false },
addNew: { type: Boolean, default: true },
questions: { type: Array, default: [] },
},
data () {
return {
allQuestions: null,
newQuestion: {
question: '',
answer: '',
}
}
},
mounted () {
this.allQuestions = (this.questions.length > 0) ? this.questions : [this.newQuestion]
},
watch: { },
computed: { },
methods: {
onAdd() {
this.allQuestions.push(this.newQuestion)
},
onRemove(key){
this.allQuestions.splice(key, 1)
}
}
}
</script>

View File

@@ -1,70 +0,0 @@
<template>
<modal :show="show" @close="$emit('close')">
<template #icon>
<svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 27C16.0681 27 15.6022 27 15.2346 26.8478C14.7446 26.6448 14.3552 26.2554 14.1522 25.7654C14 25.3978 14 24.9319 14 24V17.2C14 16.0799 14 15.5198 14.218 15.092C14.4097 14.7157 14.7157 14.4097 15.092 14.218C15.5198 14 16.0799 14 17.2 14H24C24.9319 14 25.3978 14 25.7654 14.1522C26.2554 14.3552 26.6448 14.7446 26.8478 15.2346C27 15.6022 27 16.0681 27 17M24.2 34H30.8C31.9201 34 32.4802 34 32.908 33.782C33.2843 33.5903 33.5903 33.2843 33.782 32.908C34 32.4802 34 31.9201 34 30.8V24.2C34 23.0799 34 22.5198 33.782 22.092C33.5903 21.7157 33.2843 21.4097 32.908 21.218C32.4802 21 31.9201 21 30.8 21H24.2C23.0799 21 22.5198 21 22.092 21.218C21.7157 21.4097 21.4097 21.7157 21.218 22.092C21 22.5198 21 23.0799 21 24.2V30.8C21 31.9201 21 32.4802 21.218 32.908C21.4097 33.2843 21.7157 33.5903 22.092 33.782C22.5198 34 23.0799 34 24.2 34Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template #title>
Create template
</template>
<div class="p-4">
<p>
New template will be create from your form <span class="font-semibold">{{form.title}}</span>.
Template will be public for all to create form quickly.
</p>
<form @submit.prevent="createTemplate" @keydown="templateForm.onKeydown($event)" class="mt-6">
<div class="-m-6">
<div class="border-t py-4 px-6">
<text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" />
<text-input name="slug" :form="templateForm" class="mt-4" label="Slug" :required="true" />
<rich-text-area-input name="description" :form="templateForm" class="mt-4" label="Description" :required="true" />
<text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" />
<questions-editor name="questions" :form="templateForm" class="mt-4" label="Frequently asked questions" />
</div>
<div class="flex justify-end mt-4 pb-5 px-6">
<v-button class="mr-2" :loading="templateForm.busy">Create</v-button>
<v-button color="white" @click.prevent="$emit('close')">Close</v-button>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
import Form from 'vform'
import QuestionsEditor from '../../templates/QuestionsEditor.vue';
export default {
name: 'CreateTemplateModal',
components: { QuestionsEditor },
props: {
show: { type: Boolean, required: true },
form: { type: Object, required: true }
},
data: () => ({
templateForm: new Form({
name: '',
slug: '',
description: '',
image_url: '',
})
}),
computed: {},
methods: {
async createTemplate() {
this.templateForm.form = this.form
await this.templateForm.post('/api/templates').then((response) => {
this.alertSuccess('Template was successfully created.')
this.$emit('close')
});
}
}
}
</script>

View File

@@ -69,7 +69,7 @@
</a>
<a href="#" v-if="user.template_editor"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="showCreateTemplateModal=true"
@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">
@@ -117,7 +117,7 @@
</div>
</modal>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/>
<form-template-modal :form="form" :show="showFormTemplateModal" @close="showFormTemplateModal=false"/>
</div>
</template>
@@ -125,11 +125,11 @@
import axios from 'axios'
import {mapGetters, mapState} from 'vuex'
import Dropdown from '../../../common/Dropdown.vue'
import CreateTemplateModal from '../CreateTemplateModal.vue'
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'
export default {
name: 'ExtraMenu',
components: { Dropdown, CreateTemplateModal },
components: { Dropdown, FormTemplateModal },
props: {
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
@@ -139,7 +139,7 @@ export default {
loadingDuplicate: false,
loadingDelete: false,
showDeleteFormModal: false,
showCreateTemplateModal: false
showFormTemplateModal: false
}),
computed: {

View File

@@ -0,0 +1,80 @@
<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 store from '~/store'
import TemplateTags from './TemplateTags.vue'
export default {
components: { TemplateTags },
props: {
slug: {
type: String,
required: true
}
},
data: () => ({}),
computed: {
template () {
return this.$store.getters['open/templates/getBySlug'](this.slug)
}
},
watch: {
slug () {
store.dispatch('open/templates/loadTemplate', this.slug)
}
},
mounted () {
store.dispatch('open/templates/loadTemplate', this.slug)
},
methods: {
cleanQuotes (str) {
// Remove starting and ending quotes if any
return (str) ? str.replace(/^"/, '').replace(/"$/, '') : ''
}
}
}
</script>

View File

@@ -0,0 +1,74 @@
<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>
export default {
props: {
slug: {
type: String,
required: true
},
displayAll: {
type: Boolean,
default: false
}
},
data: () => ({}),
computed: {
template () {
return this.$store.getters['open/templates/getBySlug'](this.slug)
},
types () {
if (!this.template) return null
return this.$store.getters['open/templates/getTemplateTypes'](this.template.types)
},
industries () {
if (!this.template) return null
return this.$store.getters['open/templates/getTemplateIndustries'](this.template.industries)
}
},
methods: {}
}
</script>

View File

@@ -0,0 +1,70 @@
<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-bold 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 templates" :key="template.id" class="mx-4 w-72 h-auto">
<single-template :slug="template.slug" />
</li>
</ul>
</div>
</div>
</template>
<script>
import store from '~/store'
import { mapGetters, mapState } from 'vuex'
import SingleTemplate from '../templates/SingleTemplate.vue'
export default {
components: { SingleTemplate },
props: { },
data: () => ({}),
computed: {
...mapState({
templates: state => state['open/templates'].content
})
},
watch: {
templates () {
this.$nextTick(() => {
this.setInfinite()
})
}
},
mounted() {
store.dispatch('open/templates/loadWithLimit', 10)
},
methods: {
setInfinite() {
let ul = this.$refs['templates-slider']
if(ul){
ul.insertAdjacentHTML('afterend', ul.outerHTML)
ul.nextSibling.setAttribute('aria-hidden', 'true')
}
}
}
}
</script>

View File

@@ -1,81 +0,0 @@
<template>
<div :class="wrapperClass">
<label v-if="label" :for="id?id:name"
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
>
{{ label }}
<span v-if="required" class="text-red-500 required-dot">*</span>
</label>
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
<div v-else class="my-3">
<div v-for="(questionForm, quesKey) in allQuestions" :key="quesKey" class="bg-gray-100 p-2 mb-4">
<v-button color="red" size="small" class="mb-2" @click.prevent="onRemove(quesKey)">
<svg class="h-4 w-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Remove
</v-button>
<text-input name="question" :form="questionForm" placeholder="Question title" />
<rich-text-area-input name="answer" :form="questionForm" class="mt-4" placeholder="Question response" />
</div>
<v-button v-if="addNew" color="green" size="small" nativeType="button" class="mt-2 flex" @click.prevent="onAdd">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Add New
</v-button>
</div>
<small v-if="help" :class="theme.SelectInput.help">
<slot name="help">{{ help }}</slot>
</small>
<has-error v-if="hasValidation" :form="form" :field="name" />
</div>
</template>
<script>
import inputMixin from '~/mixins/forms/input.js'
import Form from 'vform'
export default {
name: 'QuestionsEditor',
mixins: [inputMixin],
props: {
loading: { type: Boolean, default: false },
addNew: { type: Boolean, default: true }
},
data () {
return {
allQuestions: [new Form({
question: '',
answer: '',
})]
}
},
watch: {
allQuestions: {
deep: true,
handler () {
this.compVal = this.allQuestions.map((ques) => {
return ques.data()
})
}
}
},
computed: { },
methods: {
onAdd() {
this.allQuestions.push(new Form({
question: '',
answer: '',
}))
},
onRemove(key){
this.allQuestions.splice(key, 1)
}
}
}
</script>

View File

@@ -1,50 +1,191 @@
<template>
<div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4">
<breadcrumb :path="breadcrumbs"/>
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<p v-else-if="template === null || !template">
Template does not exist.
</p>
<div v-else>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
{{ template.name }}
</h2>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs">
<template #left>
<div v-if="user && (user.admin || user.template_editor)" class="ml-5">
<v-button color="gray" size="small" @click.prevent="showFormTemplateModal=true">
Edit Template
</v-button>
<form-template-modal v-if="form" :form="form" :template="template" :show="showFormTemplateModal"
@close="showFormTemplateModal=false"
/>
</div>
<div class="mb-10">
<div class="w-full shadow-xl rounded-lg my-5 max-h-72 flex items-center justify-center overflow-hidden">
<img :src="template.image_url" alt="Template cover image" class="w-full object-cover"/>
</div>
<div v-html="template.description"></div>
<div class="mt-5 text-center">
<v-button v-if="authenticated" class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
Use this template
</v-button>
<v-button v-else class="mt-4 sm:mt-0" :to="{path:'/forms/create/guest?template='+template.slug}">
Use this template
</v-button>
</div>
</template>
<template #right>
<v-button v-track.use_template_button_clicked size="small" class="mr-5"
:to="{path: createFormWithTemplateUrl}"
>
Use this template
</v-button>
</template>
</breadcrumb>
<h3 class="text-center text-gray-500 mt-8 mb-2">Template Preview</h3>
<open-complete-form ref="open-complete-form" :form="form" :creating="true"
class="mb-4 p-4 bg-gray-50 rounded-lg"/>
<div v-if="templatesLoading" class="text-center my-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<p v-else-if="template === null || !template" class="text-center my-4">
We could not find this template.
</p>
<template v-else>
<section class="pt-12 bg-gray-50 sm:pt-16 border-b pb-[250px] relative">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="flex flex-col items-center justify-center max-w-4xl gap-8 mx-auto md:gap-12 md:flex-row">
<div class="aspect-[4/3] shrink-0 rounded-lg shadow-sm overflow-hidden group max-w-xs">
<img class="object-cover w-full h-full transition-all duration-200 group-hover:scale-110"
:src="template.image_url" alt="Template cover image"
>
</div>
<div v-if="template.questions.length > 0" id="questions">
<h3 class="text-xl font-semibold mt-8">Frequently asked questions</h3>
<div class="pt-2">
<div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="my-3 border rounded-lg">
<h5 class="border-b p-2 text-gray-700 font-semibold">{{ ques.question }}</h5>
<p class="p-2 text-gray-600" v-html="ques.answer"></p>
<div class="flex-1 text-center md:text-left relative">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
{{ template.name }}
</h1>
<p class="mt-2 text-lg font-normal text-gray-600">
{{ cleanQuotes(template.short_description) }}
</p>
<template-tags :slug="template.slug" :display-all="true"
class="flex flex-wrap items-center justify-center gap-3 mt-4 md:justify-start"
/>
</div>
</div>
</div>
</section>
<section class="relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
<div class="max-w-7xl">
<div
class="max-w-2xl p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
>
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
Template Preview
</p>
<open-complete-form ref="open-complete-form" :form="form" :creating="true"
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
/>
</div>
</div>
<div class="absolute bottom-0 translate-y-full inset-x-0">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl -mt-[20px]">
<div class="flex items-center justify-center">
<v-button v-track.use_template_button_clicked class="mx-auto w-full max-w-[300px]" :to="{path: createFormWithTemplateUrl}">
Use this template
</v-button>
</div>
<div class="flex items-center justify-center">
<div class="text-left mx-auto text-gray-500 text-xs mt-4">
Core features 100% free<br>
No credit card required<br>
No submissions limit on Free plan
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="pt-20 pb-12 bg-white sm:pb-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-2xl mx-auto mt-16 space-y-12 sm:mt-16 sm:space-y-16">
<div class="nf-text" v-html="template.description" />
<template v-if="template.questions.length > 0">
<hr class="mt-12 border-gray-200">
<div>
<div class="text-center">
<h3 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
Frequently asked questions
</h3>
<p class="mt-2 text-base font-normal text-gray-600">
Everything you need to know about this template.
</p>
</div>
<dl class="mt-12 space-y-10">
<div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="space-y-4">
<dt class="font-semibold text-gray-900 dark:text-gray-100">
{{ ques.question }}
</dt>
<dd class="mt-2 leading-6 text-gray-600 dark:text-gray-400" v-html="ques.answer" />
</div>
</dl>
</div>
</template>
</div>
</div>
</section>
<section v-if="template.related_templates.length > 0" 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">
Related templates
</h4>
<v-button :to="{name:'templates'}" color="white" size="small" :arrow="true">
View All
</v-button>
</div>
<div class="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 sm:gap-y-12">
<single-template v-for="related in template.related_templates" :key="related" :slug="related" />
</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="text-center">
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
How OpnForm works
</h4>
</div>
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12">
<div
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
>
<div
class="inline-flex items-center justify-center w-10 h-10 text-base font-bold bg-white rounded-full shadow-sm ring-1 ring-inset ring-gray-200 text-blue-500 shrink-0"
>
1
</div>
<div>
<h5 class="text-base font-bold leading-tight text-gray-900">
Copy the template and change it the way you like
</h5>
<p class="mt-2 text-sm font-normal text-gray-600">
<router-link :to="{path:createFormWithTemplateUrl}">
Click here to copy this template
</router-link>
and start customizing it. Change the questions, add new ones, choose colors and
more.
</p>
</div>
</div>
<div
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
>
<div
class="inline-flex items-center justify-center w-10 h-10 text-base font-bold bg-white rounded-full shadow-sm ring-1 ring-inset ring-gray-200 text-blue-500 shrink-0"
>
2
</div>
<div>
<h5 class="text-base font-bold leading-tight text-gray-900">
Embed the form or share it via a link
</h5>
<p class="mt-2 text-sm font-normal text-gray-600">
You can directly share your form link, or embed the form on your website. It's magic! 🪄
</p>
</div>
</div>
</div>
<!-- add video here -->
<!-- <div class="max-w-5xl mx-auto mt-12 shadow-sm rounded-xl bg-blue-50 aspect-video" />-->
</div>
</section>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
@@ -52,61 +193,110 @@
<script>
import store from '~/store'
import Form from 'vform'
import {mapGetters, mapState} from 'vuex'
import Fuse from 'fuse.js'
import { mapGetters, mapState } from 'vuex'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import Breadcrumb from "../../components/common/Breadcrumb.vue";
import SeoMeta from "../../mixins/seo-meta";
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
import Breadcrumb from '../../components/common/Breadcrumb.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import TemplateTags from '../../components/pages/templates/TemplateTags.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
import FormTemplateModal from '../../components/open/forms/components/templates/FormTemplateModal.vue'
export default {
components: { Breadcrumb, OpenFormFooter, OpenCompleteForm, TemplateTags, SingleTemplate, FormTemplateModal },
mixins: [SeoMeta],
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm},
beforeRouteEnter(to, from, next) {
loadTemplates()
beforeRouteEnter (to, from, next) {
if (to.params?.slug) {
store.dispatch('open/templates/loadTemplate', to.params?.slug)
store.dispatch('open/templates/loadTypesAndIndustries')
}
next()
},
data() {
return {}
data () {
return {
showFormTemplateModal: false
}
},
mounted() {
mounted () {
},
methods: {},
methods: {
cleanQuotes (str) {
// Remove starting and ending quotes if any
return (str) ? str.replace(/^"/, '').replace(/"$/, '') : ''
}
},
computed: {
...mapGetters({
authenticated: 'auth/check'
authenticated: 'auth/check',
user: 'auth/user'
}),
...mapState({
templatesLoading: state => state['open/templates'].loading
}),
breadcrumbs() {
breadcrumbs () {
if (!this.template) {
return [{route: {name: 'templates'}, label: 'Templates'}]
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: this.template.name}]
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.template.name }]
},
template() {
template () {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
},
form() {
form () {
return new Form(this.template.structure)
},
metaTitle () {
return this.template ? this.template.name : 'Template'
return this.template ? this.template.name : 'Form Template'
},
metaDescription () {
if (!this.template) return null
// take the first 140 characters of the description
return this.template.short_description?.substring(0, 140) + '... | Customize any template and create your own form in minutes.'
},
metaImage () {
if (!this.template) return null
return this.template.image_url
},
metaTags () {
return (this.template && this.template.publicly_listed) ? [] : [{ name: 'robots', content: 'noindex' }]
},
createFormWithTemplateUrl () {
if(this.authenticated) {
return '/forms/create?template=' + this.template?.slug
}
return '/forms/create/guest?template=' + this.template?.slug
}
}
}
</script>
<style lang='scss'>
.nf-text {
@apply space-y-4;
h2 {
@apply text-sm font-normal tracking-widest text-gray-500 uppercase;
}
p {
@apply font-normal leading-7 text-gray-900 dark:text-gray-100;
}
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
.aspect-video {
aspect-ratio: 16/9;
}
</style>

View File

@@ -1,94 +1,160 @@
<template>
<div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4">
<div>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
Templates
</h2>
<div class="flex flex-col min-h-full border-t">
<section class="py-12 sm:py-16 bg-gray-50 border-b border-gray-200">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="text-center max-w-xl mx-auto">
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
Form Templates
</h1>
<p class="text-gray-600 mt-4 text-lg font-normal">
Our collection of beautiful templates to create your own forms!
</p>
</div>
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<p v-else-if="templates.length === 0">
No any templates found.
</p>
<div v-else class="mb-4">
<div v-if="templates && templates.length"
class="grid max-w-3xl grid-cols-1 mx-auto text-center sm:text-left sm:grid-cols-2 gap-y-8 gap-x-8 lg:gap-x-20">
<div class="relative group" v-for="(template, index) in templates" :key="template.id">
<div class="overflow-hidden rounded-lg aspect-w-16 aspect-h-9">
<img class="object-cover w-full h-full transition-all duration-300 transform group-hover:scale-125"
:src="template.image_url" alt=""/>
</div>
<p class="mt-3 mb-2 text-sm font-normal text-gray-600 font-pj">
{{ formatCreatedDate(template.created_at) }}</p>
<p class="text-xl font-bold text-gray-900 font-pj">{{ template.name }}</p>
<router-link :to="{params:{slug:template.slug},name:'templates.show'}" title="">
<span class="absolute inset-0" aria-hidden="true"></span>
</router-link>
</div>
</section>
<section class="bg-white py-12 sm:py-16">
<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>
</div>
</section>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import store from '~/store'
import {mapGetters, mapState} from 'vuex'
import { mapGetters, mapState } from 'vuex'
import Form from 'vform'
import Fuse from 'fuse.js'
import SeoMeta from '../../mixins/seo-meta.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import SeoMeta from "../../mixins/seo-meta";
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/load').then(() => {
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
components: { OpenFormFooter, SingleTemplate },
mixins: [SeoMeta],
components: {OpenFormFooter},
beforeRouteEnter(to, from, next) {
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
props: {
metaTitle: {type: String, default: 'Templates'},
metaDescription: {type: String, default: 'Our collection of beautiful templates to create your own forms!'}
metaTitle: { type: String, default: 'Templates' },
metaDescription: { type: String, default: 'Our collection of beautiful templates to create your own forms!' }
},
data() {
return {}
},
mounted() {
},
methods: {
formatCreatedDate(createdDate) {
const date = new Date(createdDate)
const dateTimeFormat = new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
data () {
return {
selectedType: 'all',
selectedIndustry: 'all',
searchTemplate: new Form({
search: ''
})
return dateTimeFormat.format(date)
}
},
mounted () {
},
computed: {
...mapState({
templates: state => state['open/templates'].content,
templatesLoading: state => state['open/templates'].loading
templatesLoading: state => state['open/templates'].loading,
industries: state => state['open/templates'].industries,
types: state => state['open/templates'].types
}),
}
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.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>

View File

@@ -78,6 +78,8 @@
<!-- <testimonials/>-->
<!-- </div>-->
<templates-slider />
<div class="w-full bg-blue-900 p-12 md:p-24 text-center">
<h4 class="font-semibold text-3xl text-white">Take your forms to the next level</h4>
<p class="text-gray-300 my-8">Generous, unlimited free plan.</p>
@@ -123,10 +125,11 @@ import MoreFeatures from '~/components/pages/welcome/MoreFeatures.vue'
import AiFeature from '~/components/pages/welcome/AiFeature.vue'
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import Testimonials from '../components/pages/welcome/Testimonials.vue'
import TemplatesSlider from '../components/pages/welcome/TemplatesSlider.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: {Testimonials, OpenFormFooter, Features, MoreFeatures, AiFeature},
components: {Testimonials, OpenFormFooter, Features, MoreFeatures, AiFeature, TemplatesSlider},
layout: 'default',

View File

@@ -67,8 +67,8 @@ export default [
{ path: '/forms/:slug', name: 'forms.show_public', component: page('forms/show-public.vue') },
// Templates
{ path: '/templates', name: 'templates', component: page('templates/templates.vue') },
{ path: '/templates/:slug', name: 'templates.show', component: page('templates/show.vue') },
{ path: '/form-templates', name: 'templates', component: page('templates/templates.vue') },
{ path: '/form-templates/:slug', name: 'templates.show', component: page('templates/show.vue') },
{ path: '*', component: page('errors/404.vue') }
]

View File

@@ -1,55 +1,124 @@
import Vue from 'vue'
import axios from 'axios'
export const templatesEndpoint = '/api/templates'
export const namespaced = true
// state
export const state = {
content: [],
industries: {},
types: {},
allLoaded: false,
loading: false
}
// getters
export const getters = {
getById: (state) => (id) => {
if (state.content.length === 0) return null
return state.content.find(item => item.id === id)
},
getBySlug: (state) => (slug) => {
if (state.content.length === 0) return null
return state.content.find(item => item.slug === slug)
},
getTemplateTypes: (state) => (slugs) => {
if (state.types.length === 0) return null
return Object.values(state.types).filter((val) => slugs.includes(val.slug)).map((item) => { return item.name })
},
getTemplateIndustries: (state) => (slugs) => {
if (state.industries.length === 0) return null
return Object.values(state.industries).filter((val) => slugs.includes(val.slug)).map((item) => { return item.name })
}
}
// mutations
export const mutations = {
set (state, items) {
state.content = items
state.allLoaded = true
},
append (state, items) {
const ids = items.map((item) => { return item.id })
state.content = state.content.filter((val) => !ids.includes(val.id))
state.content = state.content.concat(items)
},
addOrUpdate (state, item) {
state.content = state.content.filter((val) => val.id !== item.id)
state.content.push(item)
},
startLoading () {
remove (state, item) {
state.content = state.content.filter((val) => val.id !== item.id)
},
startLoading (state) {
state.loading = true
},
stopLoading () {
stopLoading (state) {
state.loading = false
},
setAllLoaded (state, val) {
state.allLoaded = val
}
}
// actions
export const actions = {
load (context) {
resetState (context) {
context.commit('set', [])
context.commit('stopLoading')
},
loadTypesAndIndustries (context) {
if (Object.keys(context.state.industries).length === 0) {
import('@/data/forms/templates/industries.json').then((module) => {
context.state.industries = module.default
})
}
if (Object.keys(context.state.types).length === 0) {
import('@/data/forms/templates/types.json').then((module) => {
context.state.types = module.default
})
}
},
loadTemplate (context, slug) {
context.commit('startLoading')
return axios.get('/api/templates').then((response) => {
context.commit('set', response.data)
context.dispatch('loadTypesAndIndustries')
if (context.getters.getBySlug(slug)) {
context.commit('stopLoading')
return Promise.resolve()
}
return axios.get(templatesEndpoint + '/' + slug).then((response) => {
context.commit('addOrUpdate', response.data)
context.commit('stopLoading')
}).catch((error) => {
context.commit('stopLoading')
})
},
loadIfEmpty ({ context, dispatch, state }) {
if (state.content.length === 0) {
return dispatch('load')
loadAll (context) {
context.commit('startLoading')
context.dispatch('loadTypesAndIndustries')
return axios.get(templatesEndpoint).then((response) => {
context.commit('append', response.data)
context.commit('setAllLoaded', true)
context.commit('stopLoading')
}).catch((error) => {
context.commit('stopLoading')
})
},
loadIfEmpty (context) {
if (!context.state.allLoaded) {
return context.dispatch('loadAll')
}
context.commit('stopLoading')
return Promise.resolve()
},
loadWithLimit (context, limit) {
context.commit('startLoading')
context.dispatch('loadTypesAndIndustries')
return axios.get(templatesEndpoint + '?limit=' + limit).then((response) => {
context.commit('set', response.data)
context.commit('setAllLoaded', false)
context.commit('stopLoading')
}).catch((error) => {
context.commit('stopLoading')
})
}
}