0351d front end linting (#377)

* feat: disable custom script for  trial users

* after lint fix

* frontend linting

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Favour Olayinka
2024-04-15 18:39:03 +01:00
committed by GitHub
parent 8d35fc8b1a
commit bcd45ce8a6
228 changed files with 17036 additions and 8744 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,30 @@
<template>
<div class="w-full flex flex-col">
<form-editor v-if="!formsLoading || form" ref="editor"
:is-edit="true"
@on-save="formInitialHash=null"
<form-editor
v-if="(!formsLoading || form ) && !error "
ref="editor"
:is-edit="true"
@on-save="formInitialHash = null"
/>
<div v-else-if="!formsLoading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500">
<div
v-else-if="error && !formsLoading"
class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500"
>
{{ error }}
</div>
<div v-else class="text-center mt-4 py-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto"/>
<div
v-else
class="text-center mt-4 py-6"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</template>
<script setup>
import {computed} from 'vue'
import FormEditor from "~/components/open/forms/components/FormEditor.vue";
import {hash} from "~/lib/utils.js";
import { computed } from "vue"
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
import { hash } from "~/lib/utils.js"
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
@@ -34,14 +42,19 @@ const formInitialHash = ref(null)
function isDirty() {
try {
return formInitialHash.value && updatedForm.value && formInitialHash.value !== hash(JSON.stringify(updatedForm?.value?.data() ?? null))
return (
formInitialHash.value &&
updatedForm.value &&
formInitialHash.value !==
hash(JSON.stringify(updatedForm?.value?.data() ?? null))
)
} catch (e) {
return false
}
}
function initUpdatedForm() {
if (!form || !form.value) {
if (!form.value || !form.value) {
return
}
@@ -61,11 +74,14 @@ watch(form, (form) => {
onBeforeRouteLeave((to, from, next) => {
if (isDirty()) {
return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {
})
return useAlert().confirm(
"Changes you made may not be saved. Are you sure want to leave?",
() => {
window.onbeforeunload = null
next()
},
() => {},
)
}
next()
})
@@ -80,7 +96,7 @@ onBeforeMount(() => {
}
if (!form.value && !formsStore.allLoaded) {
formsStore.loadAll(workspacesStore.currentId).then(()=>{
formsStore.loadAll(workspacesStore.currentId).then(() => {
initUpdatedForm()
})
} else {
@@ -89,9 +105,9 @@ onBeforeMount(() => {
})
useOpnSeoMeta({
title: 'Edit ' + ((form && form.value) ? form.value.title : 'Your Form')
title: "Edit " + (form.value && form.value ? form.value.title : "Your Form"),
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
</script>

View File

@@ -2,24 +2,39 @@
<div class="flex flex-col">
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
<div v-if="form.cover_picture">
<div id="cover-picture" class="max-h-56 w-full overflow-hidden flex items-center justify-center">
<img alt="Form Cover Picture" :src="form.cover_picture" class="w-full"/>
<div
id="cover-picture"
class="max-h-56 w-full overflow-hidden flex items-center justify-center"
>
<img
alt="Form Cover Picture"
:src="form.cover_picture"
class="w-full"
>
</div>
</div>
<div v-if="form.logo_picture" class="w-full p-5 relative mx-auto"
:class="{'pt-20':!form.cover_picture, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form.width === 'centered', 'max-w-7xl': (form.width === 'full' && !isIframe) }"
<div
v-if="form.logo_picture"
class="w-full p-5 relative mx-auto"
:class="{'pt-20':!form.cover_picture, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form.width === 'centered', 'max-w-7xl': (form.width === 'full' && !isIframe) }"
>
<img alt="Logo Picture" :src="form.logo_picture"
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
class="w-20 h-20 object-contain absolute left-5 transition-all"
/>
<img
alt="Logo Picture"
:src="form.logo_picture"
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
class="w-20 h-20 object-contain absolute left-5 transition-all"
>
</div>
</div>
<div class="w-full mx-auto px-4"
:class="{'mt-6':!isIframe, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form && (form.width === 'centered'), 'max-w-7xl': (form && form.width === 'full' && !isIframe)}"
<div
class="w-full mx-auto px-4"
:class="{'mt-6':!isIframe, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form && (form.width === 'centered'), 'max-w-7xl': (form && form.width === 'full' && !isIframe)}"
>
<div v-if="!formLoading && !form">
<h1 class="mt-6" v-text="'Whoops'"/>
<h1
class="mt-6"
v-text="'Whoops'"
/>
<p class="mt-6">
Unfortunately we could not find this form. It may have been deleted.
</p>
@@ -31,17 +46,22 @@
</div>
<div v-else-if="formLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</p>
</div>
<template v-else>
<div v-if="recordLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</p>
</div>
<open-complete-form v-show="!recordLoading" ref="openCompleteForm" :form="form" class="mb-10" :dark-mode="darkMode"
@password-entered="passwordEntered"
<OpenCompleteForm
v-show="!recordLoading"
ref="openCompleteForm"
:form="form"
class="mb-10"
:dark-mode="darkMode"
@password-entered="passwordEntered"
/>
</template>
</div>
@@ -70,7 +90,6 @@ const formLoading = computed(() => formsStore.loading)
const recordLoading = computed(() => recordsStore.loading)
const slug = useRoute().params.slug
const form = computed(() => formsStore.getByKey(slug))
const submitted = ref(false)
const openCompleteForm = ref(null)
@@ -145,7 +164,7 @@ onMounted(() => {
}
})
onBeforeRouteLeave((to, from) => {
onBeforeRouteLeave(() => {
document.body.classList.remove('public-page')
crisp.showChat()
disableDarkMode()
@@ -168,16 +187,16 @@ useOpnSeoMeta({
if (pageMeta.value.description) {
return pageMeta.value.description
}
return (form && form.value?.description) ? form.value?.description.substring(0, 160) : null
return (form.value && form.value?.description) ? form.value?.description.substring(0, 160) : null
},
ogImage: () => {
if (pageMeta.value.page_thumbnail) {
return pageMeta.value.page_thumbnail
}
return (form && form.value?.cover_picture) ? form.value?.cover_picture : null
return (form.value && form.value?.cover_picture) ? form.value?.cover_picture : null
},
robots: () => {
return (form && form.value?.can_be_indexed) ? null : 'noindex, nofollow'
return (form.value && form.value?.can_be_indexed) ? null : 'noindex, nofollow'
}
})
useHead({
@@ -186,7 +205,7 @@ useHead({
// Disable template if custom SEO title
return titleChunk
}
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm';
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm'
},
script: [ { src: '/widgets/iframeResizer.contentWindow.min.js' } ]
})

View File

@@ -4,10 +4,24 @@
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<a href="#" class="flex text-blue mb-2 font-semibold text-sm" @click.prevent="goBack">
<svg class="w-3 h-3 text-blue mt-1 mr-1" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
<a
href="#"
class="flex text-blue mb-2 font-semibold text-sm"
@click.prevent="goBack"
>
<svg
class="w-3 h-3 text-blue mt-1 mr-1"
viewBox="0 0 6 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 9L1 5L5 1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Go back
</a>
@@ -17,34 +31,91 @@
{{ form.title }}
</h2>
<div class="flex">
<extra-menu class="mr-2" :form="form" />
<extra-menu
class="mr-2"
:form="form"
/>
<v-button v-if="form.visibility === 'draft'" color="white" class="mr-2 text-blue-600 hidden sm:block"
@click="showDraftFormWarningNotification">
<svg class="w-6 h-6 inline -mt-1" 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" />
<v-button
v-if="form.visibility === 'draft'"
color="white"
class="mr-2 text-blue-600 hidden sm:block"
@click="showDraftFormWarningNotification"
>
<svg
class="w-6 h-6 inline -mt-1"
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" />
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</v-button>
<v-button v-else v-track.view_form_click="{ form_id: form.id, form_slug: form.slug }" target="_blank"
:href="form.share_url" color="white" class="mr-2 text-blue-600 hidden sm:block">
<svg class="w-6 h-6 inline -mt-1" 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" />
<v-button
v-else
v-track.view_form_click="{
form_id: form.id,
form_slug: form.slug,
}"
target="_blank"
:href="form.share_url"
color="white"
class="mr-2 text-blue-600 hidden sm:block"
>
<svg
class="w-6 h-6 inline -mt-1"
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" />
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</v-button>
<v-button class="text-white" :to="{ name: 'forms-slug-edit', params: { slug: slug } }">
<svg class="inline mr-1 -mt-1" width="18" height="17" viewBox="0 0 18 17" fill="none"
xmlns="http://www.w3.org/2000/svg">
<v-button
class="text-white"
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
>
<svg
class="inline mr-1 -mt-1"
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" />
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Edit form
</v-button>
@@ -52,51 +123,87 @@
</div>
<p class="text-gray-500 text-sm">
<span class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</span>
<span class="pr-1">- {{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
<span class="pr-1">{{ form.views_count }} view{{
form.views_count > 0 ? "s" : ""
}}</span>
<span class="pr-1">- {{ form.submissions_count }} submission{{
form.submissions_count > 0 ? "s" : ""
}}
</span>
<span>- Edited {{ form.last_edited_human }}</span>
</p>
<div v-if="['draft', 'closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
class="mt-2 flex items-center flex-wrap gap-3">
<span v-if="form.visibility == 'draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
<div
v-if="
['draft', 'closed'].includes(form.visibility) ||
(form.tags && form.tags.length > 0)
"
class="mt-2 flex items-center flex-wrap gap-3"
>
<span
v-if="form.visibility == 'draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Draft - not publicly accessible
</span>
<span v-else-if="form.visibility == 'closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
<span
v-else-if="form.visibility == 'closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Closed - won't accept new submissions
</span>
<span v-for="(tag, i) in form.tags" :key="tag"
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 dark:text-white dark:bg-gray-700">
<span
v-for="(tag) in form.tags"
:key="tag"
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 dark:text-white dark:bg-gray-700"
>
{{ tag }}
</span>
</div>
<p v-if="form.closes_at" class="text-yellow-500">
<span v-if="form.is_closed"> This form stopped accepting submissions on the {{
displayClosesDate
}} </span>
<span v-else> This form will stop accepting submissions on the {{ displayClosesDate }} </span>
<p
v-if="form.closes_at"
class="text-yellow-500"
>
<span v-if="form.is_closed">
This form stopped accepting submissions on the
{{ displayClosesDate }}
</span>
<span v-else>
This form will stop accepting submissions on the
{{ displayClosesDate }}
</span>
</p>
<p v-if="form.max_submissions_count > 0" class="text-yellow-500">
<span v-if="form.max_number_of_submissions_reached"> The form is now closed because it reached its limit of
{{
form.max_submissions_count
}} submissions. </span>
<span v-else> This form will stop accepting submissions after {{ form.max_submissions_count }} submissions.
<p
v-if="form.max_submissions_count > 0"
class="text-yellow-500"
>
<span v-if="form.max_number_of_submissions_reached">
The form is now closed because it reached its limit of
{{ form.max_submissions_count }} submissions.
</span>
<span v-else>
This form will stop accepting submissions after
{{ form.max_submissions_count }} submissions.
</span>
</p>
<form-cleanings class="mt-4" :form="form" />
<form-cleanings
class="mt-4"
:form="form"
/>
<div class="border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i + 1" class="mr-6">
<nuxt-link :to="{ name: tab.route }"
<li
v-for="(tab, i) in tabsList"
:key="i + 1"
class="mr-6"
>
<nuxt-link
:to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500">
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</nuxt-link>
</li>
@@ -109,31 +216,35 @@
<NuxtPage :form="form" />
</div>
</template>
<div v-else-if="loading" class="text-center w-full p-5">
<div
v-else-if="loading"
class="text-center w-full p-5"
>
<Loader class="h-6 w-6 mx-auto" />
</div>
<div v-else class="text-center w-full p-5">
<div
v-else
class="text-center w-full p-5"
>
Form not found.
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import ProTag from '~/components/global/ProTag.vue'
import VButton from '~/components/global/VButton.vue'
import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu.vue'
import FormCleanings from '../../../components/pages/forms/show/FormCleanings.vue'
import { computed } from "vue"
import VButton from "~/components/global/VButton.vue"
import ExtraMenu from "../../../components/pages/forms/show/ExtraMenu.vue"
import FormCleanings from "../../../components/pages/forms/show/FormCleanings.vue"
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: 'Home'
title: "Home",
})
const route = useRoute()
const authStore = useAuthStore()
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
@@ -141,39 +252,44 @@ const workspacesStore = useWorkspacesStore()
const slug = useRoute().params.slug
formsStore.startLoading()
const user = computed(() => authStore.user)
const form = computed(() => formsStore.getByKey(slug))
const workspace = computed(() => workspacesStore.getByKey(form?.value?.workspace_id))
const loading = computed(() => formsStore.loading || workspacesStore.loading)
const displayClosesDate = computed(() => {
if (form.value && form.value.closes_at) {
const dateObj = new Date(form.value.closes_at)
return dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0') + ' ' +
String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
return (
dateObj.getFullYear() +
"-" +
String(dateObj.getMonth() + 1).padStart(2, "0") +
"-" +
String(dateObj.getDate()).padStart(2, "0") +
" " +
String(dateObj.getHours()).padStart(2, "0") +
":" +
String(dateObj.getMinutes()).padStart(2, "0")
)
}
return ''
return ""
})
const tabsList = [
{
name: 'Submissions',
route: 'forms-slug-show-submissions'
name: "Submissions",
route: "forms-slug-show-submissions",
},
{
name: 'Integrations',
route: 'forms-slug-show-integrations'
name: "Integrations",
route: "forms-slug-show-integrations",
},
{
name: 'Analytics',
route: 'forms-slug-show-stats'
name: "Analytics",
route: "forms-slug-show-stats",
},
{
name: 'Share',
route: 'forms-slug-show-share'
}
name: "Share",
route: "forms-slug-show-share",
},
]
onMounted(() => {
@@ -185,17 +301,22 @@ onMounted(() => {
}
})
watch(() => form?.value?.id, (id) => {
if (id) {
workingFormStore.set(form.value)
}
})
watch(
() => form?.value?.id,
(id) => {
if (id) {
workingFormStore.set(form.value)
}
},
)
const goBack = () => {
useRouter().push({ name: 'home' })
useRouter().push({ name: "home" })
}
const showDraftFormWarningNotification = () => {
useAlert().warning('This form is currently in Draft mode and is not publicly accessible, You can change the form status on the edit form page.')
useAlert().warning(
"This form is currently in Draft mode and is not publicly accessible, You can change the form status on the edit form page.",
)
}
</script>

View File

@@ -1,7 +1,7 @@
<script setup>
definePageMeta({
redirect: to => {
return { name: 'forms-slug-show-submissions'}
}
redirect: () => {
return { name: "forms-slug-show-submissions" }
},
})
</script>

View File

@@ -9,76 +9,110 @@
Read, update and create data with dozens of 3rd-party integrations
</div>
<div v-if="integrationsLoading" class="my-6">
<Loader class="h-6 w-6 mx-auto"/>
<div
v-if="integrationsLoading"
class="my-6"
>
<Loader class="h-6 w-6 mx-auto" />
</div>
<div v-else-if="formIntegrationsList.length" class="my-6">
<IntegrationCard v-for="(row) in formIntegrationsList" :key="row.id" :integration="row" :form="form"/>
<div
v-else-if="formIntegrationsList.length"
class="my-6"
>
<IntegrationCard
v-for="row in formIntegrationsList"
:key="row.id"
:integration="row"
:form="form"
/>
</div>
<div class="text-gray-500 border shadow rounded-md p-5 mt-4" v-else>
<div
v-else
class="text-gray-500 border shadow rounded-md p-5 mt-4"
>
No integration yet form this form.
</div>
<h1 class="font-semibold mt-8 text-xl">
Add a new integration
</h1>
<div v-for="(section, sectionName) in sectionsList" :key="sectionName" class="mb-8">
<div
v-for="(section, sectionName) in sectionsList"
:key="sectionName"
class="mb-8"
>
<h3 class="text-gray-500">
{{ sectionName }}
</h3>
<div class="flex flex-wrap mt-2 gap-4">
<IntegrationListOption v-for="(sectionItem, sectionItemKey) in section"
@select="openIntegrationModal"
:key="sectionItemKey" :integration="sectionItem"/>
<IntegrationListOption
v-for="(sectionItem, sectionItemKey) in section"
:key="sectionItemKey"
:integration="sectionItem"
@select="openIntegrationModal"
/>
</div>
</div>
<IntegrationModal v-if="form && selectedIntegrationKey && selectedIntegration" :form="form"
:integration="selectedIntegration" :integrationKey="selectedIntegrationKey"
:show="showIntegrationModal"
@close="closeIntegrationModal"/>
<IntegrationModal
v-if="form && selectedIntegrationKey && selectedIntegration"
:form="form"
:integration="selectedIntegration"
:integration-key="selectedIntegrationKey"
:show="showIntegrationModal"
@close="closeIntegrationModal"
/>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue'
import IntegrationModal from '~/components/open/integrations/components/IntegrationModal.vue'
import { computed } from "vue"
import IntegrationModal from "~/components/open/integrations/components/IntegrationModal.vue"
const props = defineProps({
form: {type: Object, required: true}
form: { type: Object, required: true },
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: (props.form) ? 'Form Integrations - ' + props.form.title : 'Form Integrations'
title: props.form
? "Form Integrations - " + props.form.title
: "Form Integrations",
})
const alert = useAlert()
const crisp = useCrisp()
const route = useRoute()
const formIntegrationsStore = useFormIntegrationsStore()
const integrationsLoading = computed(() => formIntegrationsStore.loading)
const integrations = computed(() => formIntegrationsStore.availableIntegrations)
const sectionsList = computed(() => formIntegrationsStore.integrationsBySection)
const formIntegrationsList = computed(() => formIntegrationsStore.getAllByFormId(props.form.id))
const integrations = computed(
() => formIntegrationsStore.availableIntegrations,
)
const sectionsList = computed(
() => formIntegrationsStore.integrationsBySection,
)
const formIntegrationsList = computed(() =>
formIntegrationsStore.getAllByFormId(props.form.id),
)
let showIntegrationModal = ref(false)
let selectedIntegrationKey = ref(null)
let selectedIntegration = ref(null)
const showIntegrationModal = ref(false)
const selectedIntegrationKey = ref(null)
const selectedIntegration = ref(null)
onMounted(() => {
formIntegrationsStore.fetchFormIntegrations(props.form.id)
})
const openIntegrationModal = (itemKey) => {
if (!itemKey || !integrations.value.has(itemKey)) return alert.error('Integration not found')
if (integrations.value.get(itemKey).coming_soon) return alert.warning('This integration is not available yet')
if (!itemKey || !integrations.value.has(itemKey))
return alert.error("Integration not found")
if (integrations.value.get(itemKey).coming_soon)
return alert.warning("This integration is not available yet")
selectedIntegrationKey.value = itemKey
selectedIntegration.value = integrations.value.get(selectedIntegrationKey.value)
selectedIntegration.value = integrations.value.get(
selectedIntegrationKey.value,
)
showIntegrationModal.value = true
}
const closeIntegrationModal = () => {

View File

@@ -1,57 +1,81 @@
<template>
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
<div class="mb-20">
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
<regenerate-form-link class="sm:w-1/2 flex" :form="props.form"/>
<regenerate-form-link
class="sm:w-1/2 flex"
:form="props.form"
/>
<url-form-prefill class="sm:w-1/2" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<url-form-prefill
class="sm:w-1/2"
:form="props.form"
:extra-query-param="shareUrlForQueryParams"
/>
<embed-form-as-popup-modal class="sm:w-1/2 flex" :form="props.form"/>
<embed-form-as-popup-modal
class="sm:w-1/2 flex"
:form="props.form"
/>
</div>
<share-link class="mt-4" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<share-link
class="mt-4"
:form="props.form"
:extra-query-param="shareUrlForQueryParams"
/>
<embed-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<embed-code
class="mt-6"
:form="props.form"
:extra-query-param="shareUrlForQueryParams"
/>
<form-qr-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<advanced-form-url-settings :form="props.form" v-model="shareFormConfig"/>
<form-qr-code
class="mt-6"
:form="props.form"
:extra-query-param="shareUrlForQueryParams"
/>
<advanced-form-url-settings
v-model="shareFormConfig"
:form="props.form"
/>
</div>
</div>
</template>
<script setup>
import ShareLink from '~/components/pages/forms/show/ShareLink.vue'
import EmbedCode from '~/components/pages/forms/show/EmbedCode.vue'
import FormQrCode from '~/components/pages/forms/show/FormQrCode.vue'
import UrlFormPrefill from '~/components/pages/forms/show/UrlFormPrefill.vue'
import RegenerateFormLink from '~/components/pages/forms/show/RegenerateFormLink.vue'
import AdvancedFormUrlSettings from '~/components/open/forms/components/AdvancedFormUrlSettings.vue'
import EmbedFormAsPopupModal from '~/components/pages/forms/show/EmbedFormAsPopupModal.vue'
import ShareLink from "~/components/pages/forms/show/ShareLink.vue"
import EmbedCode from "~/components/pages/forms/show/EmbedCode.vue"
import FormQrCode from "~/components/pages/forms/show/FormQrCode.vue"
import UrlFormPrefill from "~/components/pages/forms/show/UrlFormPrefill.vue"
import RegenerateFormLink from "~/components/pages/forms/show/RegenerateFormLink.vue"
import AdvancedFormUrlSettings from "~/components/open/forms/components/AdvancedFormUrlSettings.vue"
import EmbedFormAsPopupModal from "~/components/pages/forms/show/EmbedFormAsPopupModal.vue"
const props = defineProps({
form: {type: Object, required: true}
form: { type: Object, required: true },
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: (props.form) ? 'Share Form - ' + props.form.title : 'Share Form'
title: props.form ? "Share Form - " + props.form.title : "Share Form",
})
const shareFormConfig = ref({
hide_title: false,
auto_submit: false
auto_submit: false,
})
const shareUrlForQueryParams = computed(() => {
let queryStr = ''
let queryStr = ""
for (const [key, value] of Object.entries(shareFormConfig.value)) {
if (value && value !== 'false' && value !== false) {
queryStr += '&' + encodeURIComponent(key) + "=" + encodeURIComponent(value)
if (value && value !== "false" && value !== false) {
queryStr +=
"&" + encodeURIComponent(key) + "=" + encodeURIComponent(value)
}
}
return queryStr.slice(1)

View File

@@ -3,21 +3,21 @@
<h3 class="font-semibold mt-4 text-xl">
Form Analytics (last 30 days)
</h3>
<form-stats :form="form"/>
<form-stats :form="form" />
</div>
</template>
<script setup>
import FormStats from '~/components/open/forms/components/FormStats.vue'
import FormStats from "~/components/open/forms/components/FormStats.vue"
const props = defineProps({
form: {type: Object, required: true}
form: { type: Object, required: true },
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: (props.form) ? 'Form Analytics - ' + props.form.title : 'Form Analytics'
title: props.form ? "Form Analytics - " + props.form.title : "Form Analytics",
})
</script>

View File

@@ -1,23 +1,24 @@
<template>
<form-submissions/>
<form-submissions />
</template>
<script setup>
import FormSubmissions from '~/components/open/forms/components/FormSubmissions.vue'
import FormSubmissions from "~/components/open/forms/components/FormSubmissions.vue"
const props = defineProps({
form: {type: Object, required: true}
form: { type: Object, required: true },
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
title: props.form
? "Form Submissions - " + props.form.title
: "Form Submissions",
})
onBeforeRouteLeave(() => {
useRecordsStore().resetState()
})
</script>

View File

@@ -1,36 +1,43 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
:is-guest="isGuest"
@openRegister="registerModal=true"
/>
<div v-else class="text-center mt-4 py-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<div key="2">
<create-form-base-modal
:show="showInitialFormModal"
@form-generated="formGenerated"
@close="showInitialFormModal = false"
/>
<form-editor
v-if="!workspacesLoading"
ref="editor"
class="w-full flex flex-grow"
:error="error"
:is-guest="isGuest"
@open-register="registerModal = true"
/>
<div
v-else
class="text-center mt-4 py-6"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</transition>
</div>
<quick-register :show-register-modal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"
<quick-register
:show-register-modal="registerModal"
@close="registerModal = false"
@reopen="registerModal = true"
@after-login="afterLogin"
/>
</div>
</template>
<script setup>
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
import QuickRegister from '~/components/pages/auth/components/QuickRegister.vue'
import CreateFormBaseModal from '../../../components/pages/forms/create/CreateFormBaseModal.vue'
import {initForm} from "~/composables/forms/initForm.js"
import {fetchTemplate} from "~/stores/templates.js"
import {fetchAllWorkspaces} from "~/stores/workspaces.js";
import QuickRegister from "~/components/pages/auth/components/QuickRegister.vue"
import CreateFormBaseModal from "../../../components/pages/forms/create/CreateFormBaseModal.vue"
import { initForm } from "~/composables/forms/initForm.js"
import { fetchTemplate } from "~/stores/templates.js"
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
@@ -39,26 +46,24 @@ const route = useRoute()
// Fetch the template
if (route.query.template !== undefined && route.query.template) {
const {data} = await fetchTemplate(route.query.template)
const { data } = await fetchTemplate(route.query.template)
templatesStore.save(data.value)
}
// Store values
const workspace = computed(() => workspacesStore.getCurrent)
const workspacesLoading = computed(() => workspacesStore.loading)
const form = storeToRefs(workingFormStore).content
useOpnSeoMeta({
title: 'Create a new Form for free',
title: "Create a new Form for free",
})
definePageMeta({
middleware: "guest"
middleware: "guest",
})
// Data
const stateReady = ref(false)
const loading = ref(false)
const error = ref('')
const error = ref("")
const registerModal = ref(false)
const isGuest = ref(true)
const showInitialFormModal = ref(false)
@@ -68,18 +73,20 @@ const editor = ref(null)
onMounted(() => {
// Set as guest user
workspacesStore.set([{
id: null,
name: 'Guest Workspace',
is_enterprise: false,
is_pro: false
}])
workspacesStore.set([
{
id: null,
name: "Guest Workspace",
is_enterprise: false,
is_pro: false,
},
])
form.value = initForm({}, true)
if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template)
if (template && template.structure) {
form.value = useForm({...form.value.data(), ...template.structure})
form.value = useForm({ ...form.value.data(), ...template.structure })
}
} else {
// No template loaded, ask how to start
@@ -100,6 +107,6 @@ const afterLogin = () => {
}
const formGenerated = (newForm) => {
form.value = useForm({...form.value.data(), ...newForm})
form.value = useForm({ ...form.value.data(), ...newForm })
}
</script>

View File

@@ -1,54 +1,61 @@
<template>
<div class="flex flex-wrap flex-col">
<transition name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<div key="2">
<create-form-base-modal
:show="showInitialFormModal"
@form-generated="formGenerated"
@close="showInitialFormModal = false"
/>
<form-editor v-if="form && !workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
@on-save="formInitialHash=null"
/>
<div v-else class="text-center mt-4 py-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<form-editor
v-if="form && !workspacesLoading"
ref="editor"
class="w-full flex flex-grow"
:error="error"
@on-save="formInitialHash = null"
/>
<div
v-else
class="text-center mt-4 py-6"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</transition>
</div>
</div>
</template>
<script setup>
import {watch} from 'vue'
import {initForm} from "~/composables/forms/initForm.js"
import { watch } from "vue"
import { initForm } from "~/composables/forms/initForm.js"
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
import CreateFormBaseModal from '../../../components/pages/forms/create/CreateFormBaseModal.vue'
import {fetchTemplate} from "~/stores/templates.js"
import {hash} from "~/lib/utils.js"
import {onBeforeRouteLeave} from 'vue-router'
import CreateFormBaseModal from "../../../components/pages/forms/create/CreateFormBaseModal.vue"
import { fetchTemplate } from "~/stores/templates.js"
import { hash } from "~/lib/utils.js"
import { onBeforeRouteLeave } from "vue-router"
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: 'Create a new Form'
title: "Create a new Form",
})
onBeforeRouteLeave((to, from, next) => {
if (isDirty()) {
return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {})
return useAlert().confirm(
"Changes you made may not be saved. Are you sure want to leave?",
() => {
window.onbeforeunload = null
next()
},
() => {},
)
}
next()
})
const route = useRoute()
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
@@ -56,28 +63,30 @@ const formStore = useFormsStore()
// Fetch the template
if (route.query.template !== undefined && route.query.template) {
const {data} = await fetchTemplate(route.query.template)
const { data } = await fetchTemplate(route.query.template)
templatesStore.save(data.value)
}
const {
getCurrent: workspace,
getAll: workspaces,
workspacesLoading: workspacesLoading
workspacesLoading: workspacesLoading,
} = storeToRefs(workspacesStore)
const {content: form} = storeToRefs(workingFormStore)
const { content: form } = storeToRefs(workingFormStore)
// State
const loading = ref(false)
const error = ref('')
const error = ref("")
const showInitialFormModal = ref(false)
const formInitialHash = ref(null)
watch(() => workspace, () => {
if (workspace) {
form.workspace_id = workspace.value.id
}
})
watch(
() => workspace,
() => {
if (workspace) {
form.workspace_id = workspace.value.id
}
},
)
onMounted(() => {
if (import.meta.client) {
@@ -92,12 +101,12 @@ onMounted(() => {
formStore.loadAll(workspace.value.id)
}
form.value = initForm({workspace_id: workspace.value?.id}, true)
form.value = initForm({ workspace_id: workspace.value?.id }, true)
formInitialHash.value = hash(JSON.stringify(form.value.data()))
if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template)
if (template && template.structure) {
form.value = useForm({...form.value.data(), ...template.structure})
form.value = useForm({ ...form.value.data(), ...template.structure })
}
} else {
// No template loaded, ask how to start
@@ -108,10 +117,14 @@ onMounted(() => {
// Methods
const formGenerated = (newForm) => {
form.value = useForm({...form.value.data(), ...newForm})
form.value = useForm({ ...form.value.data(), ...newForm })
}
const isDirty = () => {
return !loading.value && formInitialHash.value && formInitialHash.value !== hash(JSON.stringify(form.value.data()))
return (
!loading.value &&
formInitialHash.value &&
formInitialHash.value !== hash(JSON.stringify(form.value.data()))
)
}
</script>

View File

@@ -7,11 +7,23 @@
<h2 class="flex-grow text-gray-900">
Your Forms
</h2>
<v-button v-track.create_form_click :to="{name:'forms-create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"/>
<v-button
v-track.create_form_click
:to="{ name: 'forms-create' }"
>
<svg
class="w-4 h-4 text-white inline mr-1 -mt-1"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333"
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Create a new form
</v-button>
@@ -23,118 +35,183 @@
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl">
<div class="mt-8 pb-0">
<text-input v-if="forms.length > 0" class="mb-6 px-4" v-model="search" name="search" label="Search a form"
placeholder="Name of form to search"
<text-input
v-if="forms.length > 0"
v-model="search"
class="mb-6 px-4"
name="search"
label="Search a form"
placeholder="Name of form to search"
/>
<div v-if="allTags.length > 0" class="mb-4 px-6">
<div v-for="tag in allTags" :key="tag"
:class="[
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset cursor-pointer mr-2',
{'bg-blue-50 text-blue-600 ring-blue-500/10 dark:bg-blue-400':selectedTags.has(tag),
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:ring-blue-500/10 hover:dark:bg-blue-400':!selectedTags.has(tag)}
]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
<div
v-if="allTags.length > 0"
class="mb-4 px-6"
>
<div
v-for="tag in allTags"
:key="tag"
:class="[
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset cursor-pointer mr-2',
{
'bg-blue-50 text-blue-600 ring-blue-500/10 dark:bg-blue-400':
selectedTags.has(tag),
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:ring-blue-500/10 hover:dark:bg-blue-400':
!selectedTags.has(tag),
},
]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
>
{{ tag }}
</div>
</div>
<div v-if="!formsLoading && enrichedForms.length === 0" class="flex flex-wrap justify-center max-w-4xl">
<img class="w-56"
src="/img/pages/forms/search_notfound.png" alt="search-not-found"
/>
<div
v-if="!formsLoading && enrichedForms.length === 0"
class="flex flex-wrap justify-center max-w-4xl"
>
<img
class="w-56"
src="/img/pages/forms/search_notfound.png"
alt="search-not-found"
>
<h3 class="w-full mt-4 text-center text-gray-900 font-semibold">
No forms found
</h3>
<div v-if="isFilteringForms && enrichedForms.length === 0 && search"
class="mt-2 w-full text-center">
Your search "{{ search }}" did not match any forms. Please try again.
<div
v-if="isFilteringForms && enrichedForms.length === 0 && search"
class="mt-2 w-full text-center"
>
Your search "{{ search }}" did not match any forms. Please try
again.
</div>
<v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms-create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"/>
<v-button
v-if="forms.length === 0"
v-track.create_form_click
class="mt-4"
:to="{ name: 'forms-create' }"
>
<svg
class="w-4 h-4 text-white inline mr-1 -mt-1"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333"
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Create a new form
</v-button>
</div>
<div v-else-if="forms.length > 0" class="mb-10">
<div
v-else-if="forms.length > 0"
class="mb-10"
>
<div v-if="enrichedForms && enrichedForms.length">
<div v-for="(form) in enrichedForms" :key="form.id"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center relative"
<div
v-for="(form) in enrichedForms"
:key="form.id"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center relative"
>
<div class="flex-grow items-center truncate cursor-pointer relative">
<NuxtLink :to="{name:'forms-slug-show-submissions', params: {slug:form.slug}}"
class="absolute inset-0"/>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.title }}</span>
<div
class="flex-grow items-center truncate cursor-pointer relative"
>
<NuxtLink
:to="{name:'forms-slug-show-submissions', params: {slug:form.slug}}"
class="absolute inset-0"
/>
<span class="font-semibold text-gray-900 dark:text-white">{{
form.title
}}</span>
<ul class="flex text-gray-500 text-sm gap-4">
<li class="pr-1 mr-3">
{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}
{{ form.views_count }} view{{
form.views_count > 0 ? "s" : ""
}}
</li>
<li class="list-disc mr-3">
{{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
submission{{ form.submissions_count > 0 ? "s" : "" }}
</li>
<li class="list-disc mr-3">
Edited {{ form.last_edited_human }}
</li>
<li class='list-disc hidden lg:list-item' v-if="form.creator">
<li
v-if="form.creator"
class="list-disc hidden lg:list-item"
>
By
{{ form?.creator?.name }}
</li>
</ul>
<div v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
class="mt-1 flex items-center flex-wrap gap-3">
<span v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
<div
v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
class="mt-1 flex items-center flex-wrap gap-3"
>
<span
v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Draft
</span>
<span v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
<span
v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Closed
</span>
<span v-for="(tag,i) in form.tags" :key="tag"
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 dark:text-white dark:bg-gray-700"
<span
v-for="(tag) in form.tags"
:key="tag"
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 dark:text-white dark:bg-gray-700"
>
{{ tag }}
</span>
</div>
</div>
<extra-menu :form="form" :is-main-page="true"/>
<extra-menu
:form="form"
:is-main-page="true"
/>
</div>
</div>
</div>
<div v-if="formsLoading" class="text-center">
<Loader class="h-6 w-6 text-nt-blue mx-auto"/>
<div
v-if="formsLoading"
class="text-center"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</div>
</div>
<open-form-footer class="mt-8 border-t"/>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script setup>
import {useAuthStore} from '../stores/auth'
import {useFormsStore} from '../stores/forms'
import {useWorkspacesStore} from '../stores/workspaces'
import Fuse from 'fuse.js'
import TextInput from '../components/forms/TextInput.vue'
import ExtraMenu from '../components/pages/forms/show/ExtraMenu.vue'
import {refDebounced} from "@vueuse/core"
import { useFormsStore } from "../stores/forms"
import { useWorkspacesStore } from "../stores/workspaces"
import Fuse from "fuse.js"
import TextInput from "../components/forms/TextInput.vue"
import ExtraMenu from "../components/pages/forms/show/ExtraMenu.vue"
import { refDebounced } from "@vueuse/core"
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: 'Your Forms',
description: 'All of your OpnForm are here. Create new forms, or update your existing forms.'
title: "Your Forms",
description:
"All of your OpnForm are here. Create new forms, or update your existing forms.",
})
const authStore = useAuthStore()
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
formsStore.startLoading()
@@ -148,18 +225,17 @@ onMounted(() => {
})
// State
const {getAll: forms, loading: formsLoading, allTags} = storeToRefs(formsStore)
const showEditFormModal = ref(false)
const selectedForm = ref(null)
const search = ref('')
const {
getAll: forms,
loading: formsLoading,
allTags,
} = storeToRefs(formsStore)
const search = ref("")
const debouncedSearch = refDebounced(search, 500)
const selectedTags = ref(new Set())
// Methods
const editForm = (form) => {
selectedForm.value = form
showEditFormModal.value = true
}
const onTagClick = (tag) => {
if (selectedTags?.value?.has(tag)) {
selectedTags.value.remove(tag)
@@ -170,11 +246,14 @@ const onTagClick = (tag) => {
// Computed
const isFilteringForms = computed(() => {
return (search.value !== '' && search.value !== null) || selectedTags.value.size > 0
return (
(search.value !== "" && search.value !== null) ||
selectedTags.value.size > 0
)
})
const enrichedForms = computed(() => {
let enrichedForms = forms.value.map((form) => {
const enrichedForms = forms.value.map((form) => {
form.workspace = workspacesStore.getByKey(form.workspace_id)
return form
}).filter((form) => {
@@ -184,17 +263,13 @@ const enrichedForms = computed(() => {
return form.tags && form.tags.length ? [...selectedTags.value].every(r => form.tags.includes(r)) : false
})
if (!isFilteringForms || search.value === '' || search.value === null) {
if (!isFilteringForms || search.value === "" || search.value === null) {
return enrichedForms
}
// Fuze search
const fuzeOptions = {
keys: [
'title',
'slug',
'tags'
]
keys: ["title", "slug", "tags"],
}
const fuse = new Fuse(enrichedForms, fuzeOptions)
return fuse.search(debouncedSearch.value).map((res) => {

View File

@@ -1,81 +1,143 @@
<template>
<div>
<section class="bg-gradient-to-b relative from-white to-gray-100 py-8 sm:py-16 ">
<section
class="bg-gradient-to-b relative from-white to-gray-100 py-8 sm:py-16"
>
<div class="absolute inset-0">
<img class="w-full h-full object-cover object-top"
src="/img/pages/ai_form_builder/background-pattern.svg" alt="Page abstract background"
/>
<img
class="w-full h-full object-cover object-top"
src="/img/pages/ai_form_builder/background-pattern.svg"
alt="Page abstract background"
>
</div>
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative -mb-32 md:-mb-52 lg:-mb-72">
<div
class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative -mb-32 md:-mb-52 lg:-mb-72"
>
<div class="flex justify-center mb-5">
<div
class="relative flex items-center shadow-sm bg-white gap-x-4 rounded-full px-4 py-1 text-sm leading-6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20"
>
<span class="font-semibold text-gray-500">We're Open-Source</span><span class="h-4 w-px bg-gray-900/10"
aria-hidden="true"
/>
<span class="font-semibold text-gray-500">We're Open-Source</span><span
class="h-4 w-px bg-gray-900/10"
aria-hidden="true"
/>
<a
target="_blank" class="flex items-center gap-x-1 hover:no-underline"
href="https://github.com/jhumanj/opnform" v-track.welcome_github_click
v-track.welcome_github_click
target="_blank"
class="flex items-center gap-x-1 hover:no-underline"
href="https://github.com/jhumanj/opnform"
>
<span class="absolute inset-0" aria-hidden="true"/>
<span
class="absolute inset-0"
aria-hidden="true"
/>
Star us on GitHub
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" aria-hidden="true" class="-mr-2 h-5 w-5 text-gray-400"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
class="-mr-2 h-5 w-5 text-gray-400"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</a>
</div>
</div>
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-semibold text-gray-900 tracking-tight">
<h1
class="text-4xl sm:text-5xl lg:text-6xl font-semibold text-gray-900 tracking-tight"
>
Build
<span
class="bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-blue-400">beautiful forms</span>
class="bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-blue-400"
>beautiful forms</span>
<br>
in seconds
</h1>
<p class="mt-4 sm:mt-5 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500">
Create beautiful forms and share them anywhere. It super fast, you don't need to know how to code. Get
started
<p
class="mt-4 sm:mt-5 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500"
>
Create beautiful forms and share them anywhere. It super fast, you
don't need to know how to code. Get started
<span class="font-semibold">for free</span>!
</p>
<div class="mt-8 flex justify-center">
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
<v-button
v-if="!authenticated"
class="mr-1"
:to="{ name: 'forms-create-guest' }"
:arrow="true"
>
Create a form for FREE
</v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
<v-button
v-else
class="mr-1"
:to="{ name: 'forms-create' }"
:arrow="true"
>
Create a form for FREE
</v-button>
</div>
<div class="justify-center flex gap-2 mt-10">
<div class="flex items-center text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
<span>Unlimited forms</span>
</div>
<div class="flex items-center text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
<span>
Unlimited fields
</span>
<span> Unlimited fields </span>
</div>
<div class="flex text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
<span>Unlimited responses</span>
</div>
@@ -88,46 +150,58 @@
<div
class="-m-2 rounded-xl bg-blue-900/5 p-2 backdrop-blur-sm ring-1 ring-inset ring-blue-900/10 lg:-m-4 lg:rounded-2xl lg:p-4 w-full"
>
<img src="/img/pages/welcome/product-cover.jpg"
sizes="320px sm:650px lg:896px"
alt="Product screenshot" loading="lazy" class="rounded-md w-full shadow-2xl ring-1 ring-gray-900/10"
/>
<img
src="/img/pages/welcome/product-cover.jpg"
sizes="320px sm:650px lg:896px"
alt="Product screenshot"
loading="lazy"
class="rounded-md w-full shadow-2xl ring-1 ring-gray-900/10"
>
</div>
</div>
</div>
</section>
<div class="flex flex-col bg-gray-50 dark:bg-notion-dark">
<div class="bg-white dark:bg-notion-dark-light pt-32 md:pt-52 lg:pt-72 pb-8">
<div
class="bg-white dark:bg-notion-dark-light pt-32 md:pt-52 lg:pt-72 pb-8"
>
<div class="md:max-w-5xl md:mx-auto w-full">
<features class="pb-8"/>
<features class="pb-8" />
</div>
</div>
<ai-feature class="bg-white -mb-56"/>
<ai-feature class="bg-white -mb-56" />
<more-features class="pt-56"/>
<more-features class="pt-56" />
<pricing-table v-if="paidPlansEnabled" class="pb-20" :home-page="true">
<pricing-table
v-if="paidPlansEnabled"
class="pb-20"
:home-page="true"
>
<template #pricing-table>
<li class="flex gap-x-3">
<NuxtLink :to="{name:'pricing'}" class="flex gap-3">
<div class="w-5"/>
<NuxtLink
:to="{ name: 'pricing' }"
class="flex gap-3"
>
<div class="w-5" />
Read more about our pricing
</NuxtLink>
</li>
</template>
</pricing-table>
<!-- <div class="pt-20 pb-5 text-center bg-white dark:bg-notion-dark-light">-->
<!-- <h3 class="font-semibold text-3xl">See what people are saying</h3>-->
<!-- <p class="w-full mt-2 mb-8">-->
<!-- These are the stories of our customers who have joined us with great pleasure when using this crazy feature.-->
<!-- </p>-->
<!-- <testimonials/>-->
<!-- </div>-->
<!-- <div class="pt-20 pb-5 text-center bg-white dark:bg-notion-dark-light">-->
<!-- <h3 class="font-semibold text-3xl">See what people are saying</h3>-->
<!-- <p class="w-full mt-2 mb-8">-->
<!-- These are the stories of our customers who have joined us with great pleasure when using this crazy feature.-->
<!-- </p>-->
<!-- <testimonials/>-->
<!-- </div>-->
<templates-slider class="max-w-full mb-12"/>
<templates-slider class="max-w-full mb-12" />
<div class="w-full bg-blue-900 p-12 md:p-24 text-center">
<h4 class="font-semibold text-3xl text-white">
@@ -137,27 +211,68 @@
Generous, unlimited free plan.
</p>
<div class="mt-6 flex justify-center">
<v-button v-track.welcome_create_form_click :to="{ name: 'forms-create-guest' }" :arrow="true" color="blue">
<v-button
v-track.welcome_create_form_click
:to="{ name: 'forms-create-guest' }"
:arrow="true"
color="blue"
>
Create a form for FREE
</v-button>
</div>
<div class="flex justify-center mt-6">
<a target="_blank" :href="configLinks.twitter" class="mr-4">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<a
target="_blank"
:href="configLinks.twitter"
class="mr-4"
>
<svg
class="w-6 h-6 text-white"
viewBox="0 0 24 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.55016 19.7502C16.6045 19.7502 21.5583 12.2469 21.5583 5.74211C21.5583 5.53117 21.5536 5.31554 21.5442 5.1046C22.5079 4.40771 23.3395 3.5445 24 2.55554C23.1025 2.95484 22.1496 3.21563 21.1739 3.32898C22.2013 2.71315 22.9705 1.74572 23.3391 0.606011C22.3726 1.1788 21.3156 1.58286 20.2134 1.80085C19.4708 1.01181 18.489 0.48936 17.4197 0.314295C16.3504 0.13923 15.2532 0.321295 14.2977 0.832341C13.3423 1.34339 12.5818 2.15495 12.1338 3.14156C11.6859 4.12816 11.5754 5.23486 11.8195 6.29054C9.86249 6.19233 7.94794 5.68395 6.19998 4.79834C4.45203 3.91274 2.90969 2.66968 1.67297 1.14976C1.0444 2.23349 0.852057 3.51589 1.13503 4.73634C1.418 5.95678 2.15506 7.02369 3.19641 7.72023C2.41463 7.69541 1.64998 7.48492 0.965625 7.10617V7.1671C0.964925 8.30439 1.3581 9.40683 2.07831 10.287C2.79852 11.1672 3.80132 11.7708 4.91625 11.9952C4.19206 12.1934 3.43198 12.2222 2.69484 12.0796C3.00945 13.0577 3.62157 13.9131 4.44577 14.5266C5.26997 15.14 6.26512 15.4808 7.29234 15.5015C5.54842 16.8714 3.39417 17.6144 1.17656 17.6109C0.783287 17.6103 0.390399 17.5861 0 17.5387C2.25286 18.984 4.87353 19.7516 7.55016 19.7502Z"
fill="currentColor"
/>
</svg>
</a>
<a target="_blank" :href="configLinks.discord" class="mr-4">
<svg class="w-6 h-6 text-white" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>
<a
target="_blank"
:href="configLinks.discord"
class="mr-4"
>
<svg
class="w-6 h-6 text-white"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Discord</title>
<path
fill="currentColor"
d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
/>
</svg>
</a>
<a v-track.welcome_github_click target="_blank" :href="configLinks.github_url" class="mr-4">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 0C5.3724 0 0 5.3808 0 12.0204C0 17.3304 3.438 21.8364 8.2068 23.4252C8.8068 23.5356 9.0252 23.1648 9.0252 22.8456C9.0252 22.5612 9.0156 21.804 9.0096 20.802C5.6712 21.528 4.9668 19.1904 4.9668 19.1904C4.422 17.8008 3.6348 17.4312 3.6348 17.4312C2.5452 16.6872 3.7176 16.7016 3.7176 16.7016C4.9212 16.7856 5.5548 17.94 5.5548 17.94C6.6252 19.776 8.364 19.2456 9.0468 18.9384C9.1572 18.162 9.4668 17.6328 9.81 17.3328C7.146 17.0292 4.344 15.9972 4.344 11.3916C4.344 10.08 4.812 9.006 5.5788 8.166C5.4552 7.8624 5.0436 6.6396 5.6964 4.986C5.6964 4.986 6.7044 4.662 8.9964 6.2172C9.97532 5.95022 10.9853 5.81423 12 5.8128C13.02 5.8176 14.046 5.9508 15.0048 6.2172C17.2956 4.662 18.3012 4.9848 18.3012 4.9848C18.9564 6.6396 18.5436 7.8624 18.4212 8.166C19.1892 9.006 19.6548 10.08 19.6548 11.3916C19.6548 16.0092 16.848 17.0256 14.1756 17.3232C14.6064 17.694 14.9892 18.4272 14.9892 19.5492C14.9892 21.1548 14.9748 22.452 14.9748 22.8456C14.9748 23.1672 15.1908 23.5416 15.8004 23.424C18.19 22.6225 20.2672 21.0904 21.7386 19.0441C23.2099 16.9977 24.001 14.5408 24 12.0204C24 5.3808 18.6264 0 12 0Z"
fill="currentColor"
<a
v-track.welcome_github_click
target="_blank"
:href="configLinks.github_url"
class="mr-4"
>
<svg
class="w-6 h-6 text-white"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 0C5.3724 0 0 5.3808 0 12.0204C0 17.3304 3.438 21.8364 8.2068 23.4252C8.8068 23.5356 9.0252 23.1648 9.0252 22.8456C9.0252 22.5612 9.0156 21.804 9.0096 20.802C5.6712 21.528 4.9668 19.1904 4.9668 19.1904C4.422 17.8008 3.6348 17.4312 3.6348 17.4312C2.5452 16.6872 3.7176 16.7016 3.7176 16.7016C4.9212 16.7856 5.5548 17.94 5.5548 17.94C6.6252 19.776 8.364 19.2456 9.0468 18.9384C9.1572 18.162 9.4668 17.6328 9.81 17.3328C7.146 17.0292 4.344 15.9972 4.344 11.3916C4.344 10.08 4.812 9.006 5.5788 8.166C5.4552 7.8624 5.0436 6.6396 5.6964 4.986C5.6964 4.986 6.7044 4.662 8.9964 6.2172C9.97532 5.95022 10.9853 5.81423 12 5.8128C13.02 5.8176 14.046 5.9508 15.0048 6.2172C17.2956 4.662 18.3012 4.9848 18.3012 4.9848C18.9564 6.6396 18.5436 7.8624 18.4212 8.166C19.1892 9.006 19.6548 10.08 19.6548 11.3916C19.6548 16.0092 16.848 17.0256 14.1756 17.3232C14.6064 17.694 14.9892 18.4272 14.9892 19.5492C14.9892 21.1548 14.9748 22.452 14.9748 22.8456C14.9748 23.1672 15.1908 23.5416 15.8004 23.424C18.19 22.6225 20.2672 21.0904 21.7386 19.0441C23.2099 16.9977 24.001 14.5408 24 12.0204C24 5.3808 18.6264 0 12 0Z"
fill="currentColor"
/>
</svg>
</a>
@@ -166,48 +281,57 @@
<p class="mt-12 text-white text-lg">
The form below is an OpnForm, give it a try !
</p>
<div class="md:max-w-5xl md:mx-auto w-full bg-white rounded-md mt-6 p-4 shadow-lg">
<iframe class="mt-4" style="border:none;width:100%;" height="480px"
src="https://opnform.com/forms/opnform-contact"
<div
class="md:max-w-5xl md:mx-auto w-full bg-white rounded-md mt-6 p-4 shadow-lg"
>
<iframe
class="mt-4"
style="border: none; width: 100%"
height="480px"
src="https://opnform.com/forms/opnform-contact"
/>
</div>
</div>
<open-form-footer class="dark:border-t border-t"/>
<open-form-footer class="dark:border-t border-t" />
</div>
</div>
</template>
<script>
import {computed} from 'vue'
import {useAuthStore} from '../stores/auth'
import Features from '~/components/pages/welcome/Features.vue'
import MoreFeatures from '~/components/pages/welcome/MoreFeatures.vue'
import PricingTable from '../components/pages/pricing/PricingTable.vue'
import AiFeature from '~/components/pages/welcome/AiFeature.vue'
import Testimonials from '../components/pages/welcome/Testimonials.vue'
import TemplatesSlider from '../components/pages/welcome/TemplatesSlider.vue'
import opnformConfig from "~/opnform.config.js";
import { computed } from "vue"
import { useAuthStore } from "../stores/auth"
import Features from "~/components/pages/welcome/Features.vue"
import MoreFeatures from "~/components/pages/welcome/MoreFeatures.vue"
import PricingTable from "../components/pages/pricing/PricingTable.vue"
import AiFeature from "~/components/pages/welcome/AiFeature.vue"
import TemplatesSlider from "../components/pages/welcome/TemplatesSlider.vue"
import opnformConfig from "~/opnform.config.js"
export default {
components: {Testimonials, Features, MoreFeatures, PricingTable, AiFeature, TemplatesSlider},
layout: 'default',
components: {
Features,
MoreFeatures,
PricingTable,
AiFeature,
TemplatesSlider,
},
layout: "default",
setup() {
const authStore = useAuthStore()
defineRouteRules({
swr: 3600
swr: 3600,
})
return {
authenticated: computed(() => authStore.check),
config: opnformConfig,
runtimeConfig: useRuntimeConfig()
runtimeConfig: useRuntimeConfig(),
}
},
data: () => ({
}),
data: () => ({}),
computed: {
configLinks() {
@@ -216,12 +340,11 @@ export default {
paidPlansEnabled() {
return this.runtimeConfig.public.paidPlansEnabled
},
}
},
}
</script>
<style lang="scss" scoped>
.customer-logo-container {
max-width: 130px;
width: 100%;

View File

@@ -1,7 +1,9 @@
<template>
<div>
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex md:flex-row-reverse flex-wrap">
<div
class="w-full md:max-w-6xl mx-auto px-4 flex md:flex-row-reverse flex-wrap"
>
<div class="w-full md:w-1/2 md:p-6">
<div class="border rounded-md p-6 shadow-md sticky top-4">
<h2 class="font-semibold text-2xl">
@@ -12,7 +14,7 @@
<login-form />
</div>
</div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0">
<h1 class="font-bold">
Create beautiful forms and share them anywhere
</h1>
@@ -21,26 +23,53 @@
</p>
<div class="flex flex-wrap justify-center">
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited forms
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited fields
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited submissions
</p>
@@ -56,9 +85,9 @@
import LoginForm from "~/components/pages/auth/components/LoginForm.vue"
definePageMeta({
middleware: "guest"
middleware: "guest",
})
useOpnSeoMeta({
title: 'Login'
title: "Login",
})
</script>

View File

@@ -5,14 +5,29 @@
<h1 class="my-6">
Reset password
</h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" class="mb-4" />
<form
@submit.prevent="send"
@keydown="form.onKeydown($event)"
>
<alert-success
:form="form"
:message="status"
class="mb-4"
/>
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<text-input
name="email"
:form="form"
label="Email"
:required="true"
/>
<!-- Submit Button -->
<v-button class="w-full" :loading="form.busy">
<v-button
class="w-full"
:loading="form.busy"
>
Send Password Reset Link
</v-button>
</form>
@@ -24,30 +39,30 @@
<script>
export default {
setup () {
setup() {
definePageMeta({
middleware: "guest"
middleware: "guest",
})
useOpnSeoMeta({
title: 'Reset Password'
title: "Reset Password",
})
},
data: () => ({
status: '',
status: "",
form: useForm({
email: ''
})
email: "",
}),
}),
methods: {
async send () {
const { data } = await this.form.post('/password/email')
async send() {
const { data } = await this.form.post("/password/email")
this.status = data.status
this.form.reset()
}
}
},
},
}
</script>

View File

@@ -5,24 +5,48 @@
<h1 class="my-6">
Reset Password
</h1>
<form @submit.prevent="reset" @keydown="form.onKeydown($event)">
<alert-success class="mb-4" :form="form" :message="status" />
<form
@submit.prevent="reset"
@keydown="form.onKeydown($event)"
>
<alert-success
class="mb-4"
:form="form"
:message="status"
/>
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<text-input
name="email"
:form="form"
label="Email"
:required="true"
/>
<!-- Password -->
<text-input native-type="password"
name="password" :form="form" label="Password" :required="true"
<text-input
native-type="password"
name="password"
:form="form"
label="Password"
:required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password" class="mb-5"
name="password_confirmation" :form="form" label="Confirm Password" :required="true"
<text-input
native-type="password"
class="mb-5"
name="password_confirmation"
:form="form"
label="Confirm Password"
:required="true"
/>
<!-- Submit Button -->
<v-button class="w-full" :loading="form.busy">
<v-button
class="w-full"
:loading="form.busy"
>
Reset Password
</v-button>
</form>
@@ -34,38 +58,38 @@
<script>
export default {
setup () {
setup() {
definePageMeta({
middleware: "guest"
middleware: "guest",
})
useOpnSeoMeta({
title: 'Reset Password'
title: "Reset Password",
})
},
data: () => ({
status: '',
status: "",
form: useForm({
token: '',
email: '',
password: '',
password_confirmation: ''
})
token: "",
email: "",
password: "",
password_confirmation: "",
}),
}),
created () {
created() {
this.form.email = this.$route.query.email
this.form.token = this.$route.params.token
},
methods: {
async reset () {
const { data } = await this.form.post('/password/reset')
async reset() {
const { data } = await this.form.post("/password/reset")
this.status = data.status
this.form.reset()
}
}
},
},
}
</script>

View File

@@ -1,172 +1,301 @@
<template>
<div>
<section class="relative py-12 bg-gradient-to-b from-white to-gray-100 sm:py-16 lg:py-20 xl:py-24">
<section
class="relative py-12 bg-gradient-to-b from-white to-gray-100 sm:py-16 lg:py-20 xl:py-24"
>
<div class="relative px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
<h1
class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl"
>
Simple, transparent pricing. No surprises.
</h1>
<p
class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-500 sm:mt-5 sm:text-xl sm:leading-9">
Just like our codebase, our pricing is 100% transparent. One flat price for all features. No hidden fees.
class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-500 sm:mt-5 sm:text-xl sm:leading-9"
>
Just like our codebase, our pricing is 100% transparent. One flat
price for all features. No hidden fees.
</p>
</div>
</div>
</section>
<pricing-table/>
<pricing-table />
<section class="py-12 bg-white sm:py-16 lg:py-24 xl:py-24">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight">
<span class="text-blue-600">99%</span> of features are available to all users for free and without
limits.
<h2
class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight"
>
<span class="text-blue-600">99%</span> of features are available to
all users for free and without limits.
</h2>
</div>
<div
class="grid max-w-5xl grid-cols-2 mx-auto mt-12 text-center gap-y-8 gap-x-4 sm:grid-cols-3 md:gap-x-12 md:text-left sm:mt-16">
class="grid max-w-5xl grid-cols-2 mx-auto mt-12 text-center gap-y-8 gap-x-4 sm:grid-cols-3 md:gap-x-12 md:text-left sm:mt-16"
>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 9H21M7.8 3H16.2C17.8802 3 18.7202 3 19.362 3.32698C19.9265 3.6146 20.3854 4.07354 20.673 4.63803C21 5.27976 21 6.11984 21 7.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Unlimited forms
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 3.5V2M5.06066 5.06066L4 4M5.06066 13L4 14.0607M13 5.06066L14.0607 4M3.5 9H2M8.5 8.5L12.6111 21.2778L15.5 18.3889L19.1111 22L22 19.1111L18.3889 15.5L21.2778 12.6111L8.5 8.5Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Unlimited submissions
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.8 10C18.9201 10 19.4802 10 19.908 9.78201C20.2843 9.59027 20.5903 9.28431 20.782 8.90798C21 8.48016 21 7.92011 21 6.8V6.2C21 5.0799 21 4.51984 20.782 4.09202C20.5903 3.7157 20.2843 3.40973 19.908 3.21799C19.4802 3 18.9201 3 17.8 3L6.2 3C5.0799 3 4.51984 3 4.09202 3.21799C3.71569 3.40973 3.40973 3.71569 3.21799 4.09202C3 4.51984 3 5.07989 3 6.2L3 6.8C3 7.9201 3 8.48016 3.21799 8.90798C3.40973 9.28431 3.71569 9.59027 4.09202 9.78201C4.51984 10 5.07989 10 6.2 10L17.8 10Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.8 21C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4802 21 18.9201 21 17.8V17.2C21 16.0799 21 15.5198 20.782 15.092C20.5903 14.7157 20.2843 14.4097 19.908 14.218C19.4802 14 18.9201 14 17.8 14L6.2 14C5.0799 14 4.51984 14 4.09202 14.218C3.71569 14.4097 3.40973 14.7157 3.21799 15.092C3 15.5198 3 16.0799 3 17.2L3 17.8C3 18.9201 3 19.4802 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21H17.8Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Unlimited fields
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 11L12 14L22 4M16 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H16.2C17.8802 21 18.7202 21 19.362 20.673C19.9265 20.3854 20.3854 19.9265 20.673 19.362C21 18.7202 21 17.8802 21 16.2V12"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Multiple input types
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 11V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.71569 21.2843 5.40973 20.908 5.21799C20.4802 5 19.9201 5 18.8 5H5.2C4.0799 5 3.51984 5 3.09202 5.21799C2.71569 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.0799 2 8.2V11.8C2 12.9201 2 13.4802 2.21799 13.908C2.40973 14.2843 2.71569 14.5903 3.09202 14.782C3.51984 15 4.0799 15 5.2 15H11M12 10H12.005M17 10H17.005M7 10H7.005M19.25 17V15.25C19.25 14.2835 18.4665 13.5 17.5 13.5C16.5335 13.5 15.75 14.2835 15.75 15.25V17M12.25 10C12.25 10.1381 12.1381 10.25 12 10.25C11.8619 10.25 11.75 10.1381 11.75 10C11.75 9.86193 11.8619 9.75 12 9.75C12.1381 9.75 12.25 9.86193 12.25 10ZM17.25 10C17.25 10.1381 17.1381 10.25 17 10.25C16.8619 10.25 16.75 10.1381 16.75 10C16.75 9.86193 16.8619 9.75 17 9.75C17.1381 9.75 17.25 9.86193 17.25 10ZM7.25 10C7.25 10.1381 7.13807 10.25 7 10.25C6.86193 10.25 6.75 10.1381 6.75 10C6.75 9.86193 6.86193 9.75 7 9.75C7.13807 9.75 7.25 9.86193 7.25 10ZM15.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7951 20.7951 20.6422 20.891 20.454C21 20.2401 21 19.9601 21 19.4V18.6C21 18.0399 21 17.7599 20.891 17.546C20.7951 17.3578 20.6422 17.2049 20.454 17.109C20.2401 17 19.9601 17 19.4 17H15.6C15.0399 17 14.7599 17 14.546 17.109C14.3578 17.2049 14.2049 17.3578 14.109 17.546C14 17.7599 14 18.0399 14 18.6V19.4C14 19.9601 14 20.2401 14.109 20.454C14.2049 20.6422 14.3578 20.7951 14.546 20.891C14.7599 21 15.0399 21 15.6 21Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Form password
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 15C16.3431 15 15 16.3431 15 18C15 19.6569 16.3431 21 18 21C19.6569 21 21 19.6569 21 18C21 16.3431 19.6569 15 18 15ZM18 15V8C18 7.46957 17.7893 6.96086 17.4142 6.58579C17.0391 6.21071 16.5304 6 16 6H13M6 9C7.65685 9 9 7.65685 9 6C9 4.34315 7.65685 3 6 3C4.34315 3 3 4.34315 3 6C3 7.65685 4.34315 9 6 9ZM6 9V21"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Webhooks
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21 8H3M16 2V5M8 2V5M12 18V12M9 15H15M7.8 22H16.2C17.8802 22 18.7202 22 19.362 21.673C19.9265 21.3854 20.3854 20.9265 20.673 20.362C21 19.7202 21 18.8802 21 17.2V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
Closing date
</p>
</div>
<div class="flex flex-col items-center col-span-2 gap-3 md:flex-row sm:col-span-1">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<div
class="flex flex-col items-center col-span-2 gap-3 md:flex-row sm:col-span-1"
>
<svg
aria-hidden="true"
class="w-6 h-6 shrink-0 stroke-blue-600"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6C12.5523 6 13 5.55228 13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5C11 5.55228 11.4477 6 12 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 20C12.5523 20 13 19.5523 13 19C13 18.4477 12.5523 18 12 18C11.4477 18 11 18.4477 11 19C11 19.5523 11.4477 20 12 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 6C19.5523 6 20 5.55228 20 5C20 4.44772 19.5523 4 19 4C18.4477 4 18 4.44772 18 5C18 5.55228 18.4477 6 19 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 13C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 12.5523 18.4477 13 19 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 20C19.5523 20 20 19.5523 20 19C20 18.4477 19.5523 18 19 18C18.4477 18 18 18.4477 18 19C18 19.5523 18.4477 20 19 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 13C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 20C5.55228 20 6 19.5523 6 19C6 18.4477 5.55228 18 5 18C4.44772 18 4 18.4477 4 19C4 19.5523 4.44772 20 5 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
<p
class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950"
>
And much more...
</p>
</div>
</div>
<div class="max-w-3xl p-6 mx-auto mt-12 sm:mt-16 bg-yellow-50 ring-1 ring-inset ring-yellow-200 rounded-2xl">
<div
class="max-w-3xl p-6 mx-auto mt-12 sm:mt-16 bg-yellow-50 ring-1 ring-inset ring-yellow-200 rounded-2xl"
>
<div class="flex items-start gap-4">
<svg aria-hidden="true" class="w-8 h-8 shrink-0 stroke-yellow-500" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-8 h-8 shrink-0 stroke-yellow-500"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21L11.8999 20.8499C11.2053 19.808 10.858 19.287 10.3991 18.9098C9.99286 18.5759 9.52476 18.3254 9.02161 18.1726C8.45325 18 7.82711 18 6.57482 18H5.2C4.07989 18 3.51984 18 3.09202 17.782C2.71569 17.5903 2.40973 17.2843 2.21799 16.908C2 16.4802 2 15.9201 2 14.8V6.2C2 5.07989 2 4.51984 2.21799 4.09202C2.40973 3.71569 2.71569 3.40973 3.09202 3.21799C3.51984 3 4.07989 3 5.2 3H5.6C7.84021 3 8.96031 3 9.81596 3.43597C10.5686 3.81947 11.1805 4.43139 11.564 5.18404C12 6.03968 12 7.15979 12 9.4M12 21V9.4M12 21L12.1001 20.8499C12.7947 19.808 13.142 19.287 13.6009 18.9098C14.0071 18.5759 14.4752 18.3254 14.9784 18.1726C15.5467 18 16.1729 18 17.4252 18H18.8C19.9201 18 20.4802 18 20.908 17.782C21.2843 17.5903 21.5903 17.2843 21.782 16.908C22 16.4802 22 15.9201 22 14.8V6.2C22 5.07989 22 4.51984 21.782 4.09202C21.5903 3.71569 21.2843 3.40973 20.908 3.21799C20.4802 3 19.9201 3 18.8 3H18.4C16.1598 3 15.0397 3 14.184 3.43597C13.4314 3.81947 12.8195 4.43139 12.436 5.18404C12 6.03968 12 7.15979 12 9.4"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div>
<p class="text-lg font-semibold text-yellow-600">
Nonprofit & Student Discount 50%
</p>
<p class="mt-1 text-base font-medium leading-7 text-yellow-600">
Whether your nonprofit is large or small, OpnForms online Form Builder helps your organization help
others. It takes just a few minutes to create and publish your forms online. As an exclusive benefit,
we offer nonprofits & students a 50-percent discount!
Whether your nonprofit is large or small, OpnForms online Form
Builder helps your organization help others. It takes just a few
minutes to create and publish your forms online. As an exclusive
benefit, we offer nonprofits & students a 50-percent discount!
</p>
</div>
</div>
@@ -174,46 +303,64 @@
</div>
</section>
<section class="py-12 bg-gray-50 border-t border-gray-200 sm:py-16 lg:py-20 xl:py-24">
<section
class="py-12 bg-gray-50 border-t border-gray-200 sm:py-16 lg:py-20 xl:py-24"
>
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight">
<h2
class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight"
>
Got any question?
</h2>
<p class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-600 sm:text-lg sm:leading-8">
<p
class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-600 sm:text-lg sm:leading-8"
>
We've compiled a list of the most common questions we get asked.
</p>
</div>
<dl
class="grid max-w-5xl grid-cols-1 mx-auto mt-12 gap-y-12 sm:grid-cols-2 sm:mt-16 sm:gap-x-8 sm:gap-y-12 lg:gap-x-10">
<div v-for="q in [
{
'question':'Is there a free version of OpnForm available?',
'answer':'Yes, OpnForm offers a free version with access to 99% of features, including unlimited forms, submissions, fields, and more. Our goal is to provide robust functionality for all users without limitations.',
},
{
'question':'What does the Pro Plan include?',
'answer':'The OpnForm Pro Plan is designed to meet the advanced needs of teams and creators. It includes features like form confirmation emails, Slack and Discord notifications, editable submissions, custom domain, custom code integration, larger file uploads, removal of OpnForm branding, priority support, and more.',
},
{
'question':'Can I try the Pro Plan before subscribing?',
'answer':'Absolutely! We offer a free 3-day trial of the OpnForm Pro Plan. This allows you to explore all the empowering features and experience the value it brings to your form-building process. The trial is automatically applied.',
},
{
'question':'Is there a discount for annual plans?',
'answer':'Yes, we offer a 20% discount for annual Pro Plan subscriptions. By choosing the yearly billing option, you can enjoy the same great features at a reduced cost.',
},
{
'question':'How does the nonprofit and student discount work?',
'answer':'OpnForm is committed to supporting nonprofits and students. We provide an exclusive 50% discount on the Pro Plan for nonprofit organizations and students. This discount helps you make the most of our form builder while staying within your budget.',
},
{
'question':'Can I cancel or change my plan at any time?',
'answer':'Yes, you have the flexibility to upgrade, downgrade, or cancel your OpnForm Pro Plan at any time. Changes will take effect immediately, and you\'ll only be billed based on the plan you\'re currently on.',
},
]" :key="q.question">
<dt class="text-base font-semibold leading-7 text-gray-950 sm:text-lg sm:leading-8">
class="grid max-w-5xl grid-cols-1 mx-auto mt-12 gap-y-12 sm:grid-cols-2 sm:mt-16 sm:gap-x-8 sm:gap-y-12 lg:gap-x-10"
>
<div
v-for="q in [
{
question: 'Is there a free version of OpnForm available?',
answer:
'Yes, OpnForm offers a free version with access to 99% of features, including unlimited forms, submissions, fields, and more. Our goal is to provide robust functionality for all users without limitations.',
},
{
question: 'What does the Pro Plan include?',
answer:
'The OpnForm Pro Plan is designed to meet the advanced needs of teams and creators. It includes features like form confirmation emails, Slack and Discord notifications, editable submissions, custom domain, custom code integration, larger file uploads, removal of OpnForm branding, priority support, and more.',
},
{
question: 'Can I try the Pro Plan before subscribing?',
answer:
'Absolutely! We offer a free 3-day trial of the OpnForm Pro Plan. This allows you to explore all the empowering features and experience the value it brings to your form-building process. The trial is automatically applied.',
},
{
question: 'Is there a discount for annual plans?',
answer:
'Yes, we offer a 20% discount for annual Pro Plan subscriptions. By choosing the yearly billing option, you can enjoy the same great features at a reduced cost.',
},
{
question: 'How does the nonprofit and student discount work?',
answer:
'OpnForm is committed to supporting nonprofits and students. We provide an exclusive 50% discount on the Pro Plan for nonprofit organizations and students. This discount helps you make the most of our form builder while staying within your budget.',
},
{
question: 'Can I cancel or change my plan at any time?',
answer:
'Yes, you have the flexibility to upgrade, downgrade, or cancel your OpnForm Pro Plan at any time. Changes will take effect immediately, and you\'ll only be billed based on the plan you\'re currently on.',
},
]"
:key="q.question"
>
<dt
class="text-base font-semibold leading-7 text-gray-950 sm:text-lg sm:leading-8"
>
{{ q.question }}
</dt>
<dd class="mt-2 text-base font-medium leading-7 text-gray-600">
@@ -224,55 +371,61 @@
<div class="mt-12 text-center sm:mt-16">
<p class="text-base font-medium text-gray-950">
Didn't find the answer? <a href="#" @click.prevent="contactUs"
class="font-semibold text-blue-600 hover:underline">Contact us</a>
Didn't find the answer?
<a
href="#"
class="font-semibold text-blue-600 hover:underline"
@click.prevent="contactUs"
>Contact us</a>
</p>
</div>
</div>
</section>
<open-form-footer/>
<open-form-footer />
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth';
import PricingTable from '../components/pages/pricing/PricingTable.vue'
import { computed } from "vue"
import { useAuthStore } from "../stores/auth"
import PricingTable from "../components/pages/pricing/PricingTable.vue"
export default {
components: {PricingTable},
layout: 'default',
components: { PricingTable },
layout: "default",
setup () {
setup() {
useOpnSeoMeta({
title: 'Pricing',
description: 'All of our core features are free, and there is no quantity limit. You can also created more advanced and customized forms with OpnForms Pro.'
title: "Pricing",
description:
"All of our core features are free, and there is no quantity limit. You can also created more advanced and customized forms with OpnForms Pro.",
})
definePageMeta({
middleware: [
function (to, from) {
function () {
// Custom inline middleware
if (!useRuntimeConfig().public.paidPlansEnabled) { // If no paid plan so no need this page
return navigateTo('/', { redirectCode: 301 })
if (!useRuntimeConfig().public.paidPlansEnabled) {
// If no paid plan so no need this page
return navigateTo("/", { redirectCode: 301 })
}
},
],
});
})
const authStore = useAuthStore()
return {
user : computed(() => authStore.user),
authenticated : computed(() => authStore.check)
user: computed(() => authStore.user),
authenticated: computed(() => authStore.check),
}
},
methods: {
contactUs() {
useCrisp().openAndShowChat()
}
}
},
},
}
</script>

View File

@@ -5,7 +5,10 @@
<h1 class="sm:text-5xl">
Privacy Policy
</h1>
<NotionPage :block-map="blockMap" :loading="loading" />
<NotionPage
:block-map="blockMap"
:loading="loading"
/>
</div>
</div>
<open-form-footer />
@@ -13,19 +16,21 @@
</template>
<script setup>
import {useNotionPagesStore} from "~/stores/notion_pages.js";
import {computed} from "vue";
import { useNotionPagesStore } from "~/stores/notion_pages.js"
import { computed } from "vue"
useOpnSeoMeta({
title: 'Privacy Policy'
title: "Privacy Policy",
})
defineRouteRules({
swr: 3600
swr: 3600,
})
const notionPageStore = useNotionPagesStore()
await notionPageStore.load('9c97349ceda7455aab9b341d1ff70f79')
await notionPageStore.load("9c97349ceda7455aab9b341d1ff70f79")
const loading = computed(() => notionPageStore.loading)
const blockMap = computed(() => notionPageStore.getByKey('9c97349ceda7455aab9b341d1ff70f79'))
const blockMap = computed(() =>
notionPageStore.getByKey("9c97349ceda7455aab9b341d1ff70f79"),
)
</script>

View File

@@ -1,7 +1,9 @@
<template>
<div>
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex items-center md:flex-row-reverse flex-wrap">
<div
class="w-full md:max-w-6xl mx-auto px-4 flex items-center md:flex-row-reverse flex-wrap"
>
<div class="w-full lg:w-1/2 md:p-6">
<app-sumo-register class="mb-10 p-6 lg:hidden" />
<div class="border rounded-md p-6 shadow-md sticky top-4">
@@ -12,7 +14,7 @@
<register-form />
</div>
</div>
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 ">
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0">
<app-sumo-register class="mb-10" />
<h1 class="font-bold">
Create beautiful forms and share them anywhere
@@ -22,26 +24,53 @@
</p>
<div class="flex flex-wrap justify-center">
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited forms
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited fields
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
Unlimited submissions
</p>
@@ -60,28 +89,27 @@ import AppSumoRegister from "~/components/vendor/appsumo/AppSumoRegister.vue"
export default {
components: {
AppSumoRegister,
RegisterForm
RegisterForm,
},
setup() {
useOpnSeoMeta({
title: 'Register'
title: "Register",
})
definePageMeta({
middleware: "guest"
middleware: "guest",
})
defineRouteRules({
swr: 3600
swr: 3600,
})
},
data: () => ({
}),
data: () => ({}),
computed: {},
methods: {}
methods: {},
}
</script>

View File

@@ -1,90 +1,95 @@
<template>
<div class="bg-white">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<nuxt-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</nuxt-link>
</li>
</ul>
</div>
<div class="bg-white">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<NuxtPage />
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li
v-for="(tab, i) in tabsList"
:key="i + 1"
class="mr-6"
>
<nuxt-link
:to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</nuxt-link>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<NuxtPage />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth'
<script setup>
import { computed } from "vue"
import { useAuthStore } from "../stores/auth"
definePageMeta({
middleware: "auth"
})
definePageMeta({
middleware: "auth",
})
useOpnSeoMeta({
title: 'Settings'
})
useOpnSeoMeta({
title: "Settings",
})
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const tabsList = computed(() => {
const tabs = [
{
name: 'Profile',
route: 'settings-profile'
},
{
name: 'Workspace Settings',
route: 'settings-workspace'
},
{
name: 'Password',
route: 'settings-password'
},
{
name: 'Delete Account',
route: 'settings-account'
}
]
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const tabsList = computed(() => {
const tabs = [
{
name: "Profile",
route: "settings-profile",
},
{
name: "Workspace Settings",
route: "settings-workspace",
},
{
name: "Password",
route: "settings-password",
},
{
name: "Delete Account",
route: "settings-account",
},
]
if (user?.value?.is_subscribed) {
tabs.splice(1, 0, {
name: 'Billing',
route: 'settings-billing'
})
}
if (user?.value?.is_subscribed) {
tabs.splice(1, 0, {
name: "Billing",
route: "settings-billing",
})
}
if (user?.value?.admin) {
tabs.push({
name: 'Admin',
route: 'settings-admin'
})
}
if (user?.value?.admin) {
tabs.push({
name: "Admin",
route: "settings-admin",
})
}
return tabs
})
</script>
return tabs
})
</script>

View File

@@ -1,45 +1,58 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Danger zone</h3>
<h3 class="font-semibold text-2xl text-gray-900">
Danger zone
</h3>
<p class="text-gray-600 text-sm mt-2">
This will permanently delete your entire account. All your forms, submissions and workspaces will be deleted.
<span class="text-red-500">
This cannot be undone.
</span>
This will permanently delete your entire account. All your forms,
submissions and workspaces will be deleted.
<span class="text-red-500"> This cannot be undone. </span>
</p>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4" color="red" @click="useAlert().confirm('Do you really want to delete your account?',deleteAccount)">
<v-button
:loading="loading"
class="mt-4"
color="red"
@click="
useAlert().confirm(
'Do you really want to delete your account?',
deleteAccount,
)
"
>
Delete account
</v-button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { useRouter } from "vue-router"
const router = useRouter()
const authStore = useAuthStore()
let loading = false
useOpnSeoMeta({
title: 'Account'
title: "Account",
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
const deleteAccount = () => {
loading = true
opnFetch('/user', {method:'DELETE'}).then(async (data) => {
loading = false
useAlert().success(data.message)
opnFetch("/user", { method: "DELETE" })
.then(async (data) => {
loading = false
useAlert().success(data.message)
authStore.logout()
router.push({ name: 'login' })
}).catch((error) => {
useAlert().error(error.data.message)
loading = false
})
authStore.logout()
router.push({ name: "login" })
})
.catch((error) => {
useAlert().error(error.data.message)
loading = false
})
}
</script>

View File

@@ -1,83 +1,108 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Admin settings</h3>
<h3 class="font-semibold text-2xl text-gray-900">
Admin settings
</h3>
<small class="text-gray-600">Manage settings.</small>
<h3 class="mt-3 text-lg font-semibold mb-4">
Tools
</h3>
<div class="flex flex-wrap mb-5">
<a :href="statsUrl" target="_blank">
<v-button class="mx-1" color="gray" shade="lighter">
Stats
</v-button>
<a
:href="statsUrl"
target="_blank"
>
<v-button
class="mx-1"
color="gray"
shade="lighter"
> Stats </v-button>
</a>
<a :href="horizonUrl" target="_blank">
<v-button class="mx-1" color="gray" shade="lighter">
Horizon
</v-button>
<a
:href="horizonUrl"
target="_blank"
>
<v-button
class="mx-1"
color="gray"
shade="lighter"
> Horizon </v-button>
</a>
</div>
<h3 class="text-lg font-semibold mb-4">
Impersonate User
</h3>
<form @submit.prevent="impersonate" @keydown="form.onKeydown($event)">
<form
@submit.prevent="impersonate"
@keydown="form.onKeydown($event)"
>
<!-- Password -->
<text-input name="identifier" :form="form" label="Identifier"
:required="true" help="User Id, User Email or Form Slug"
<text-input
name="identifier"
:form="form"
label="Identifier"
:required="true"
help="User Id, User Email or Form Slug"
/>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4">Impersonate User</v-button>
<v-button
:loading="loading"
class="mt-4"
>
Impersonate User
</v-button>
</form>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import {opnFetch} from "~/composables/useOpnApi.js";
import {fetchAllWorkspaces} from "~/stores/workspaces.js";
import { useRouter } from "vue-router"
import { opnFetch } from "~/composables/useOpnApi.js"
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
definePageMeta({
middleware: "moderator"
middleware: "moderator",
})
useOpnSeoMeta({
title: 'Admin'
title: "Admin",
})
const authStore = useAuthStore()
const workspacesStore = useWorkspacesStore()
const router = useRouter()
let form = useForm({
identifier: ''
const form = useForm({
identifier: "",
})
const loading = ref(false)
const runtimeConfig = useRuntimeConfig()
const statsUrl = runtimeConfig.public.apiBase + '/stats'
const horizonUrl = runtimeConfig.public.apiBase + '/horizon'
const statsUrl = runtimeConfig.public.apiBase + "/stats"
const horizonUrl = runtimeConfig.public.apiBase + "/horizon"
const impersonate = () => {
loading.value = true
authStore.startImpersonating()
opnFetch('/admin/impersonate/' + encodeURI(form.identifier)).then(async (data) => {
// Save the token.
authStore.setToken(data.token, false)
opnFetch("/admin/impersonate/" + encodeURI(form.identifier))
.then(async (data) => {
// Save the token.
authStore.setToken(data.token, false)
// Fetch the user.
const userData = await opnFetch('user')
authStore.setUser(userData)
const workspaces = await fetchAllWorkspaces()
workspacesStore.set(workspaces.data.value)
loading.value = false
// Fetch the user.
const userData = await opnFetch("user")
authStore.setUser(userData)
const workspaces = await fetchAllWorkspaces()
workspacesStore.set(workspaces.data.value)
loading.value = false
router.push({ name: 'home' })
}).catch((error) => {
console.error(error)
useAlert().error(error.data.message)
loading.value = false
})
router.push({ name: "home" })
})
.catch((error) => {
console.error(error)
useAlert().error(error.data.message)
loading.value = false
})
}
</script>

View File

@@ -5,11 +5,16 @@
</h3>
<template v-if="user.has_customer_id">
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it at any
time.</small>
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it
at any time.</small>
<div class="mt-4">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
<v-button
color="gray"
shade="light"
:loading="billingLoading"
@click.prevent="openBillingDashboard"
>
Manage Subscription
</v-button>
</div>
@@ -20,30 +25,33 @@
</template>
<script setup>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue'
import { computed } from "vue"
import { useAuthStore } from "../../stores/auth"
import AppSumoBilling from "../../components/vendor/appsumo/AppSumoBilling.vue"
useOpnSeoMeta({
title: 'Billing'
title: "Billing",
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
const authStore = useAuthStore()
let user = computed(() => authStore.user)
const user = computed(() => authStore.user)
let billingLoading = false
const openBillingDashboard = () => {
billingLoading = true
opnFetch('/subscription/billing-portal').then((data) => {
const url = data.portal_url
window.location = url
}).catch((error) => {
useAlert().error(error.data.message)
}).finally(() => {
billingLoading = false
})
opnFetch("/subscription/billing-portal")
.then((data) => {
const url = data.portal_url
window.location = url
})
.catch((error) => {
useAlert().error(error.data.message)
})
.finally(() => {
billingLoading = false
})
}
</script>

View File

@@ -1,7 +1,7 @@
<script setup>
definePageMeta({
redirect: to => {
return { name: 'settings-profile'}
}
redirect: () => {
return { name: "settings-profile" }
},
})
</script>

View File

@@ -5,19 +5,34 @@
</h3>
<small class="text-gray-600">Manage your password.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<form
class="mt-3"
@submit.prevent="update"
@keydown="form.onKeydown($event)"
>
<!-- Password -->
<text-input native-type="password"
name="password" :form="form" label="Password" :required="true"
<text-input
native-type="password"
name="password"
:form="form"
label="Password"
:required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password"
name="password_confirmation" :form="form" label="Confirm Password" :required="true"
<text-input
native-type="password"
name="password_confirmation"
:form="form"
label="Confirm Password"
:required="true"
/>
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">
<v-button
:loading="form.busy"
class="mt-4"
>
Update password
</v-button>
</form>
@@ -26,23 +41,26 @@
<script setup>
useOpnSeoMeta({
title: 'Password'
title: "Password",
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
let form = useForm({
password: '',
password_confirmation: ''
const form = useForm({
password: "",
password_confirmation: "",
})
const update = () => {
form.patch('/settings/password').then((response) => {
form.reset()
useAlert().success('Password updated.')
}).catch((error) => {
console.error(error)
})
form
.patch("/settings/password")
.then(() => {
form.reset()
useAlert().success("Password updated.")
})
.catch((error) => {
console.error(error)
})
}
</script>

View File

@@ -5,15 +5,32 @@
</h3>
<small class="text-gray-600">Update your username and manage your account details.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<form
class="mt-3"
@submit.prevent="update"
@keydown="form.onKeydown($event)"
>
<!-- Name -->
<text-input name="name" :form="form" label="Name" :required="true" />
<text-input
name="name"
:form="form"
label="Name"
:required="true"
/>
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<text-input
name="email"
:form="form"
label="Email"
:required="true"
/>
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">
<v-button
:loading="form.busy"
class="mt-4"
>
Save changes
</v-button>
</form>
@@ -25,27 +42,27 @@ const authStore = useAuthStore()
const user = computed(() => authStore.user)
useOpnSeoMeta({
title: 'Profile'
title: "Profile",
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
let form = useForm({
name: '',
email: ''
const form = useForm({
name: "",
email: "",
})
const update = () => {
form.patch('/settings/profile').then((response) => {
form.patch("/settings/profile").then((response) => {
authStore.updateUser(response)
useAlert().success('Your info has been updated!')
useAlert().success("Your info has been updated!")
})
}
onBeforeMount(() => {
// Fill the form with user data.
form.keys().forEach(key => {
form.keys().forEach((key) => {
form[key] = user.value[key]
})
})

View File

@@ -7,29 +7,48 @@
</h3>
<small class="text-gray-600">Manage your workspaces.</small>
</div>
<v-button color="outline-blue" :loading="loading" @click="workspaceModal=true">
<svg class="inline -mt-1 mr-1 h-4 w-4" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg"
<v-button
color="outline-blue"
:loading="loading"
@click="workspaceModal = true"
>
<svg
class="inline -mt-1 mr-1 h-4 w-4"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"
<path
d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333"
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Create new workspace
</v-button>
</div>
<div v-if="loading" class="w-full text-blue-500 text-center">
<div
v-if="loading"
class="w-full text-blue-500 text-center"
>
<Loader class="h-10 w-10 p-5" />
</div>
<div v-else-if="workspace">
<div class="mt-4 flex group bg-white items-center">
<div class="flex space-x-4 flex-grow items-center">
<img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12"
/>
<div v-else class="rounded-2xl bg-gray-100 h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
<img
v-if="isUrl(workspace.icon)"
:src="workspace.icon"
:alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12"
>
<div
v-else
class="rounded-2xl bg-gray-100 h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
/>
<div class="space-y-4 py-1">
<div class="font-bold truncate">
@@ -40,38 +59,70 @@
</div>
<template v-if="customDomainsEnabled">
<text-area-input :form="customDomainsForm" name="custom_domains" class="mt-4" :required="false"
:disabled="!workspace.is_pro"
label="Workspace Custom Domains" wrapper-class="" placeholder="yourdomain.com - 1 per line"
<text-area-input
:form="customDomainsForm"
name="custom_domains"
class="mt-4"
:required="false"
:disabled="!workspace.is_pro"
label="Workspace Custom Domains"
wrapper-class=""
placeholder="yourdomain.com - 1 per line"
/>
<p class="text-gray-500 text-sm">
Read our <a href="#"
@click.prevent="crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')"
>custom domain instructions</a> to learn how to use your own domain.
Read our
<a
href="#"
@click.prevent="
crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')
"
>custom domain instructions</a>
to learn how to use your own domain.
</p>
</template>
<div class="flex flex-wrap justify-between gap-2 mt-4">
<v-button v-if="customDomainsEnabled" class="w-full sm:w-auto" :loading="customDomainsLoading"
@click="saveChanges">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
<v-button
v-if="customDomainsEnabled"
class="w-full sm:w-auto"
:loading="customDomainsLoading"
@click="saveChanges"
>
<svg
class="w-4 h-4 text-white inline mr-1 -mt-1"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Save Domains
</v-button>
<v-button v-if="workspaces.length > 1" color="white" class="group w-full sm:w-auto" :loading="loading"
@click="deleteWorkspace(workspace.id)"
<v-button
v-if="workspaces.length > 1"
color="white"
class="group w-full sm:w-auto"
:loading="loading"
@click="deleteWorkspace(workspace.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 inline group-hover:text-red-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 -mt-1 inline group-hover:text-red-700"
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"
<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>
Remove workspace
@@ -80,12 +131,24 @@
</div>
<!-- Workspace modal -->
<modal :show="workspaceModal" max-width="lg" @close="workspaceModal=false">
<modal
:show="workspaceModal"
max-width="lg"
@close="workspaceModal = false"
>
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
class="w-8 h-8"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -93,18 +156,32 @@
Create Workspace
</template>
<div class="px-4">
<form @submit.prevent="createWorkspace" @keydown="form.onKeydown($event)">
<form
@submit.prevent="createWorkspace"
@keydown="form.onKeydown($event)"
>
<div>
<text-input name="name" class="mt-4" :form="form" :required="true"
label="Workspace Name"
<text-input
name="name"
class="mt-4"
:form="form"
:required="true"
label="Workspace Name"
/>
<text-input name="emoji" class="mt-4" :form="form" :required="false"
label="Emoji"
<text-input
name="emoji"
class="mt-4"
:form="form"
:required="false"
label="Emoji"
/>
</div>
<div class="w-full mt-6">
<v-button :loading="form.busy" class="w-full my-3">
<v-button
:loading="form.busy"
class="w-full my-3"
>
Save
</v-button>
</div>
@@ -115,37 +192,42 @@
</template>
<script setup>
import {watch} from "vue";
import {fetchAllWorkspaces} from "~/stores/workspaces.js";
import { watch } from "vue"
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
const crisp = useCrisp()
const workspacesStore = useWorkspacesStore()
const workspaces = computed(() => workspacesStore.getAll)
let loading = computed(() => workspacesStore.loading)
const loading = computed(() => workspacesStore.loading)
useOpnSeoMeta({
title: 'Workspaces'
title: "Workspaces",
})
definePageMeta({
middleware: "auth"
middleware: "auth",
})
let form = useForm({
name: '',
emoji: ''
const form = useForm({
name: "",
emoji: "",
})
let workspaceModal = ref(false)
let customDomainsForm = useForm({
custom_domain: ''
const workspaceModal = ref(false)
const customDomainsForm = useForm({
custom_domain: "",
})
let customDomainsLoading = ref(false)
const customDomainsLoading = ref(false)
let workspace = computed(() => workspacesStore.getCurrent)
let customDomainsEnabled = computed(() => useRuntimeConfig().public.customDomainsEnabled)
const workspace = computed(() => workspacesStore.getCurrent)
const customDomainsEnabled = computed(
() => useRuntimeConfig().public.customDomainsEnabled,
)
watch(() => workspace, () => {
initCustomDomains()
})
watch(
() => workspace,
() => {
initCustomDomains()
},
)
onMounted(() => {
fetchAllWorkspaces()
@@ -157,56 +239,72 @@ const saveChanges = () => {
customDomainsLoading.value = true
// Update the workspace custom domain
customDomainsForm.put('/open/workspaces/' + workspace.value.id + '/custom-domains', {
data: {
custom_domains: customDomainsForm?.custom_domains?.split('\n')
.map(domain => domain ? domain.trim() : null)
.filter(domain => domain && domain.length > 0)
}
}).then((data) => {
workspacesStore.save(data)
useAlert().success('Custom domains saved.')
}).catch((error) => {
useAlert().error('Failed to update custom domains: ' + error.response.data.message)
}).finally(() => {
customDomainsLoading.value = false
})
customDomainsForm
.put("/open/workspaces/" + workspace.value.id + "/custom-domains", {
data: {
custom_domains: customDomainsForm?.custom_domains
?.split("\n")
.map((domain) => (domain ? domain.trim() : null))
.filter((domain) => domain && domain.length > 0),
},
})
.then((data) => {
workspacesStore.save(data)
useAlert().success("Custom domains saved.")
})
.catch((error) => {
useAlert().error(
"Failed to update custom domains: " + error.response.data.message,
)
})
.finally(() => {
customDomainsLoading.value = false
})
}
const initCustomDomains = () => {
if (!workspace || !workspace.value.custom_domains) return
customDomainsForm.custom_domains = workspace.value?.custom_domains.join('\n')
customDomainsForm.custom_domains = workspace.value?.custom_domains.join("\n")
}
const deleteWorkspace = (workspaceId) => {
if (workspaces.length <= 1) {
useAlert().error('You cannot delete your only workspace.')
useAlert().error("You cannot delete your only workspace.")
return
}
useAlert().confirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
opnFetch('/open/workspaces/' + workspaceId, {method: 'DELETE'}).then((data) => {
useAlert().success('Workspace successfully removed.')
workspacesStore.remove(workspaceId)
})
})
useAlert().confirm(
"Do you really want to delete this workspace? All forms created in this workspace will be removed.",
() => {
opnFetch("/open/workspaces/" + workspaceId, { method: "DELETE" }).then(
() => {
useAlert().success("Workspace successfully removed.")
workspacesStore.remove(workspaceId)
},
)
},
)
}
const isUrl = (str) => {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
const pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i",
) // fragment locator
return !!pattern.test(str)
}
const createWorkspace = () => {
form.post('/open/workspaces/create').then((data) => {
form.post("/open/workspaces/create").then((data) => {
workspacesStore.save(data.workspace)
workspacesStore.currentId = data.workspace.id
workspaceModal.value = false
useAlert().success('Workspace successfully created! You are now editing settings for your new workspace.')
useAlert().success(
"Workspace successfully created! You are now editing settings for your new workspace.",
)
})
}
</script>

View File

@@ -1,33 +1,34 @@
<template />
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { computed } from "vue"
import { useAuthStore } from "../../stores/auth"
export default {
components: { },
layout: 'default',
middleware: 'auth',
components: {},
layout: "default",
middleware: "auth",
setup () {
setup() {
useOpnSeoMeta({
title: 'Error'
title: "Error",
})
const authStore = useAuthStore()
return {
authenticated : computed(() => authStore.check),
authenticated: computed(() => authStore.check),
}
},
data: () => ({
}),
data: () => ({}),
mounted () {
this.$router.push({ name: 'pricing' })
useAlert().error('Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.')
computed: {},
mounted() {
this.$router.push({ name: "pricing" })
useAlert().error(
"Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.",
)
},
computed: {}
}
</script>

View File

@@ -1,6 +1,8 @@
<template>
<div class="flex flex-col min-h-screen">
<div class="w-full md:max-w-3xl md:mx-auto px-4 mb-10 md:pb-20 md:pt-16 text-center flex-grow">
<div
class="w-full md:max-w-3xl md:mx-auto px-4 mb-10 md:pb-20 md:pt-16 text-center flex-grow"
>
<h1 class="text-4xl font-semibold">
Thank you!
</h1>
@@ -16,63 +18,71 @@
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { computed } from "vue"
import { useAuthStore } from "../../stores/auth"
export default {
layout: 'default',
middleware: 'auth',
layout: "default",
middleware: "auth",
setup () {
setup() {
useOpnSeoMeta({
title: 'Subscription Success'
title: "Subscription Success",
})
const authStore = useAuthStore()
return {
authStore,
authenticated : computed(() => authStore.check),
user : computed(() => authStore.user),
crisp: useCrisp()
authenticated: computed(() => authStore.check),
user: computed(() => authStore.user),
crisp: useCrisp(),
}
},
data: () => ({
interval: null
interval: null,
}),
mounted () {
computed: {},
mounted() {
this.redirectIfSubscribed()
this.interval = setInterval(() => this.checkSubscription(), 5000)
},
beforeUnmount () {
beforeUnmount() {
clearInterval(this.interval)
},
methods: {
async checkSubscription () {
async checkSubscription() {
// Fetch the user.
await this.authStore.fetchUser()
this.redirectIfSubscribed()
},
redirectIfSubscribed () {
redirectIfSubscribed() {
if (this.user.is_subscribed) {
useAmplitude().logEvent('subscribed', { plan: this.user.has_enterprise_subscription ? 'enterprise' : 'pro' })
this.crisp.pushEvent('subscribed', { plan: this.user.has_enterprise_subscription ? 'enterprise' : 'pro' })
this.$router.push({ name: 'home' })
useAmplitude().logEvent("subscribed", {
plan: this.user.has_enterprise_subscription ? "enterprise" : "pro",
})
this.crisp.pushEvent("subscribed", {
plan: this.user.has_enterprise_subscription ? "enterprise" : "pro",
})
this.$router.push({ name: "home" })
if (this.user.has_enterprise_subscription) {
useAlert().success('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise ' +
'features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌')
useAlert().success(
"Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise " +
"features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌",
)
} else {
useAlert().success('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro ' +
'features. Feel free to contact us if you have any question 🙌')
useAlert().success(
"Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro " +
"features. Feel free to contact us if you have any question 🙌",
)
}
}
}
},
},
computed: {}
}
</script>

View File

@@ -1,52 +1,89 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs" v-if="template">
<template #left>
<div v-if="canEditTemplate" 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>
</template>
<template #right>
<v-button v-if="canEditTemplate" v-track.copy_template_button_clicked size="small" color="white" class="mr-5"
@click.prevent="copyTemplateUrl"
<breadcrumb
v-if="template"
:path="breadcrumbs"
>
<template #left>
<div
v-if="canEditTemplate"
class="ml-5"
>
<v-button
color="gray"
size="small"
@click.prevent="showFormTemplateModal = true"
>
Copy Template URL
Edit Template
</v-button>
<v-button v-track.use_template_button_clicked size="small" class="mr-5"
:to="createFormWithTemplateUrl"
>
Use this template
</v-button>
</template>
<form-template-modal
v-if="form"
:form="form"
:template="template"
:show="showFormTemplateModal"
@close="showFormTemplateModal = false"
/>
</div>
</template>
<template #right>
<v-button
v-if="canEditTemplate"
v-track.copy_template_button_clicked
size="small"
color="white"
class="mr-5"
@click.prevent="copyTemplateUrl"
>
Copy Template URL
</v-button>
<v-button
v-track.use_template_button_clicked
size="small"
class="mr-5"
:to="createFormWithTemplateUrl"
>
Use this template
</v-button>
</template>
</breadcrumb>
<p v-if="template === null || !template" class="text-center my-4">
<p
v-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-sm">
<img class="object-cover w-full transition-all duration-200 group-hover:scale-110 h-[240px]"
:src="template.image_url" alt="Template cover image" width="500px" height="380px"
/>
<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-sm"
>
<img
class="object-cover w-full transition-all duration-200 group-hover:scale-110 h-[240px]"
:src="template.image_url"
alt="Template cover image"
width="500px"
height="380px"
>
</div>
<div class="flex-1 text-center md:text-left relative">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<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 :template="template" :display-all="true"
class="flex flex-wrap items-center justify-center gap-3 mt-4 md:justify-start"
<template-tags
:template="template"
:display-all="true"
class="flex flex-wrap items-center justify-center gap-3 mt-4 md:justify-start"
/>
</div>
</div>
@@ -61,8 +98,11 @@
<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"
<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>
@@ -70,8 +110,11 @@
<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="createFormWithTemplateUrl">
<v-button
v-track.use_template_button_clicked
class="mx-auto w-full max-w-[300px]"
:to="createFormWithTemplateUrl"
>
Use this template
</v-button>
</div>
@@ -88,14 +131,21 @@
<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"/>
<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">
<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">
@@ -103,11 +153,18 @@
</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">
<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"/>
<dd
class="mt-2 leading-6 text-gray-600 dark:text-gray-400"
v-html="ques.answer"
/>
</div>
</dl>
</div>
@@ -116,20 +173,35 @@
</div>
</section>
<section v-if="relatedTemplates && relatedTemplates.length > 0"
class="py-12 bg-white border-t border-gray-200 sm:py-16">
<section
v-if="relatedTemplates && relatedTemplates.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">
<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">
<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 relatedTemplates" :key="related.id" :template="related"/>
<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 relatedTemplates"
:key="related.id"
:template="related"
/>
</div>
</div>
</section>
@@ -137,7 +209,9 @@
<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">
<h4
class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl"
>
How OpnForm works
</h4>
</div>
@@ -159,8 +233,8 @@
<NuxtLink :to="createFormWithTemplateUrl">
Click here to copy this template
</NuxtLink>
and start customizing it. Change the questions, add new ones, choose colors and
more.
and start customizing it. Change the questions, add new ones,
choose colors and more.
</p>
</div>
</div>
@@ -178,7 +252,8 @@
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! 🪄
You can directly share your form link, or embed the form on
your website. It's magic! 🪄
</p>
</div>
</div>
@@ -190,23 +265,23 @@
</section>
</template>
<open-form-footer class="mt-8 border-t"/>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script setup>
import {computed} from 'vue'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
import {fetchTemplate} from "~/stores/templates.js"
import FormTemplateModal from '~/components/open/forms/components/templates/FormTemplateModal.vue'
import { computed } from "vue"
import OpenCompleteForm from "../../components/open/forms/OpenCompleteForm.vue"
import Breadcrumb from "~/components/global/Breadcrumb.vue"
import SingleTemplate from "../../components/pages/templates/SingleTemplate.vue"
import { fetchTemplate } from "~/stores/templates.js"
import FormTemplateModal from "~/components/open/forms/components/templates/FormTemplateModal.vue"
defineRouteRules({
swr: 3600
swr: 3600,
})
const {copy} = useClipboard()
const { copy } = useClipboard()
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
@@ -218,19 +293,24 @@ const form = computed(() => template.value.structure)
// Fetch the template
if (!template.value) {
const {data} = await fetchTemplate(slug.value)
const { data } = await fetchTemplate(slug.value)
templatesStore.save(data.value)
}
// Fetch related templates
const {data: relatedTemplatesData} = await useAsyncData('related-templates', () => {
return Promise.all(template.value.related_templates.map((slug) => {
if (templatesStore.getByKey(slug)) {
return Promise.resolve(templatesStore.getByKey(slug))
}
return fetchTemplate(slug).then((res) => res.data.value)
}))
})
const { data: relatedTemplatesData } = await useAsyncData(
"related-templates",
() => {
return Promise.all(
template.value.related_templates.map((slug) => {
if (templatesStore.getByKey(slug)) {
return Promise.resolve(templatesStore.getByKey(slug))
}
return fetchTemplate(slug).then((res) => res.data.value)
}),
)
},
)
templatesStore.save(relatedTemplatesData.value)
templatesStore.initTypesAndIndustries()
@@ -240,49 +320,67 @@ const showFormTemplateModal = ref(false)
// Computed
const breadcrumbs = computed(() => {
if (!template.value) {
return [{route: {name: 'templates'}, label: 'Templates'}]
return [{ route: { name: "templates" }, label: "Templates" }]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: template.value.name}]
return [
{ route: { name: "templates" }, label: "Templates" },
{ label: template.value.name },
]
})
const relatedTemplates = computed(() => templatesStore.getByKey(template?.value?.related_templates))
const canEditTemplate = computed(() => authStore.check && template.value && (authStore.user.admin || authStore.user.template_editor || template.creator_id === authStore.user.id))
const relatedTemplates = computed(() =>
templatesStore.getByKey(template?.value?.related_templates),
)
const canEditTemplate = computed(
() =>
authStore.check &&
template.value &&
(authStore.user.admin ||
authStore.user.template_editor ||
template.value.creator_id === authStore.user.id),
)
const createFormWithTemplateUrl = computed(() => {
return {name: (authStore.check) ? 'forms-create' : 'forms-create-guest', query: {template: template?.value?.slug}}
return {
name: authStore.check ? "forms-create" : "forms-create-guest",
query: { template: template?.value?.slug },
}
})
// methods
const cleanQuotes = (str) => {
// Remove starting and ending quotes if any
return (str) ? str.replace(/^"/, '').replace(/"$/, '') : ''
return str ? str.replace(/^"/, "").replace(/"$/, "") : ""
}
const copyTemplateUrl = () => {
copy(template.value.share_url)
useAlert().success('Copied!')
useAlert().success("Copied!")
}
useOpnSeoMeta({
title: () => {
if (!template || !template.value) return 'Form Template'
if (!template.value || !template.value) return "Form Template"
return template.value.name
},
description() {
if (!template || !template.value) return null
if (!template.value || !template.value) return null
// take the first 140 characters of the description
return template.value.short_description?.substring(0, 140) + '... | Customize any template and create your own form in minutes.'
return (
template.value.short_description?.substring(0, 140) +
"... | Customize any template and create your own form in minutes."
)
},
ogImage() {
if (!template || !template.value) return null
if (!template.value || !template.value) return null
return template.value.image_url
},
robots: () => {
if (!template || !template.value) return null
return template.value.publicly_listed ? null : 'noindex'
}
if (!template.value || !template.value) return null
return template.value.publicly_listed ? null : "noindex"
},
})
</script>
<style lang='scss'>
<style lang="scss">
.nf-text {
@apply space-y-4;
h2 {

View File

@@ -3,7 +3,9 @@
<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">
<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">
@@ -13,22 +15,26 @@
</div>
</section>
<templates-list :templates="templates" :loading="loading"/>
<templates-list
:templates="templates"
:loading="loading"
/>
<open-form-footer class="mt-8 border-t"/>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script setup>
import {loadAllTemplates} from "~/stores/templates.js";
import { loadAllTemplates } from "~/stores/templates.js"
defineRouteRules({
swr: 3600
swr: 3600,
})
useOpnSeoMeta({
title: 'Form Templates',
description: 'Our collection of beautiful templates to create your own forms!'
title: "Form Templates",
description:
"Our collection of beautiful templates to create your own forms!",
})
const templatesStore = useTemplatesStore()

View File

@@ -1,8 +1,11 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs"/>
<breadcrumb :path="breadcrumbs" />
<p v-if="industry === null || !industry" class="text-center my-4">
<p
v-if="industry === null || !industry"
class="text-center my-4"
>
We could not find this industry.
</p>
<template v-else>
@@ -12,7 +15,9 @@
<div class="font-semibold sm:w-full text-blue-500 mb-3">
{{ industry.name }}
</div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
<h1
class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900"
>
{{ industry.meta_title }}
</h1>
<p class="max-w-xl mx-auto text-gray-600 mt-4 text-lg font-normal">
@@ -22,8 +27,11 @@
</div>
</section>
<templates-list :templates="templates" :filter-industries="false" :show-industries="false">
<templates-list
:templates="templates"
:filter-industries="false"
:show-industries="false"
>
<template #before-lists>
<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">
@@ -36,66 +44,77 @@
</templates-list>
</template>
<open-form-footer class="mt-8 border-t"/>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script setup>
import {computed} from 'vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import {loadAllTemplates} from "~/stores/templates.js";
import { computed } from "vue"
import Breadcrumb from "~/components/global/Breadcrumb.vue"
import { loadAllTemplates } from "~/stores/templates.js"
defineRouteRules({
swr: 3600
swr: 3600,
})
const route = useRoute()
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
loadAllTemplates(templatesStore)
// Computed
const authenticated = computed(() => authStore.check)
const user = computed(() => authStore.user)
const templates = computed(() => templatesStore.getAll.filter((item) => {
return (item.industries && item.industries.length > 0) ? item.industries.includes(route.params.slug) : false
}))
const templates = computed(() =>
templatesStore.getAll.filter((item) => {
return item.industries && item.industries.length > 0
? item.industries.includes(route.params.slug)
: false
}),
)
const breadcrumbs = computed(() => {
if (!industry) {
return [{route: {name: 'templates'}, label: 'Templates'}]
if (!industry.value) {
return [{ route: { name: "templates" }, label: "Templates" }]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: industry.value.name}]
return [
{ route: { name: "templates" }, label: "Templates" },
{ label: industry.value.name },
]
})
const industry = computed(() => templatesStore.industries.get(route.params.slug))
const industry = computed(() =>
templatesStore.industries.get(route.params.slug),
)
useOpnSeoMeta({
title: () => {
if (!industry.value) return 'Form Templates'
if (!industry.value) return "Form Templates"
if (industry.value.meta_title.length > 60) {
return industry.value.meta_title
}
return industry.value.meta_title
},
description: () => industry.value ? industry.value.meta_description: 'Our collection of beautiful templates to create your own forms!'
description: () =>
industry.value
? industry.value.meta_description
: "Our collection of beautiful templates to create your own forms!",
})
useHead({
titleTemplate: (titleChunk) => {
// Disable title template for longer titles
if (industry.value
&& industry.value.meta_title.length < 60
&& !industry.value.meta_title.toLowerCase().includes('opnform')
if (
industry.value &&
industry.value.meta_title.length < 60 &&
!industry.value.meta_title.toLowerCase().includes("opnform")
) {
return titleChunk ? `${titleChunk} - OpnForm` : 'Form Templates - OpnForm'
return titleChunk
? `${titleChunk} - OpnForm`
: "Form Templates - OpnForm"
}
return titleChunk ? titleChunk : 'Form Templates - OpnForm'
}
return titleChunk ? titleChunk : "Form Templates - OpnForm"
},
})
</script>
<style lang='scss'>
<style lang="scss">
.nf-text {
@apply space-y-4;
h2 {
@@ -115,4 +134,3 @@ useHead({
}
}
</style>

View File

@@ -3,7 +3,9 @@
<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">
<h1
class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900"
>
My Form Templates
</h1>
<p class="text-gray-600 mt-4 text-lg font-normal">
@@ -13,26 +15,32 @@
</div>
</section>
<templates-list :templates="templates" :loading="loading" :show-types="false" :show-industries="false"/>
<templates-list
:templates="templates"
:loading="loading"
:show-types="false"
:show-industries="false"
/>
</div>
</template>
<script setup>
definePageMeta({
middleware: "auth"
middleware: "auth",
})
useOpnSeoMeta({
title: 'My Templates',
description: 'Our collection of beautiful templates to create your own forms!'
title: "My Templates",
description:
"Our collection of beautiful templates to create your own forms!",
})
let loading = ref(false)
let templates = ref([])
const loading = ref(false)
const templates = ref([])
onMounted(() => {
loading.value = true
opnFetch('templates',{query: {onlymy: true}}).then((data) => {
opnFetch("templates", { query: { onlymy: true } }).then((data) => {
loading.value = false
templates.value = data
})

View File

@@ -1,8 +1,11 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs"/>
<breadcrumb :path="breadcrumbs" />
<p v-if="type === null || !type" class="text-center my-4">
<p
v-if="type === null || !type"
class="text-center my-4"
>
We could not find this type.
</p>
<template v-else>
@@ -12,7 +15,9 @@
<div class="font-semibold sm:w-full text-blue-500 mb-3">
{{ type.name }}
</div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
<h1
class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900"
>
{{ type.meta_title }}
</h1>
<p class="max-w-xl mx-auto text-gray-600 mt-4 text-lg font-normal">
@@ -22,8 +27,11 @@
</div>
</section>
<templates-list :templates="templates" :filter-types="false" :show-industries="false">
<templates-list
:templates="templates"
:filter-types="false"
:show-industries="false"
>
<template #before-lists>
<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">
@@ -36,67 +44,76 @@
</templates-list>
</template>
<open-form-footer class="mt-8 border-t"/>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script setup>
import {computed} from 'vue'
import OpenFormFooter from '../../../components/pages/OpenFormFooter.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import {loadAllTemplates} from "~/stores/templates.js";
import { computed } from "vue"
import OpenFormFooter from "../../../components/pages/OpenFormFooter.vue"
import Breadcrumb from "~/components/global/Breadcrumb.vue"
import { loadAllTemplates } from "~/stores/templates.js"
defineRouteRules({
swr: 3600
swr: 3600,
})
const route = useRoute()
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
loadAllTemplates(templatesStore)
// Computed
const authenticated = computed(() => authStore.check)
const user = computed(() => authStore.user)
const templates = computed(() => templatesStore.getAll.filter((item) => {
return (item.types && item.types.length > 0) ? item.types.includes(route.params.slug) : false
}))
const templates = computed(() =>
templatesStore.getAll.filter((item) => {
return item.types && item.types.length > 0
? item.types.includes(route.params.slug)
: false
}),
)
const breadcrumbs = computed(() => {
if (!type) {
return [{route: {name: 'templates'}, label: 'Templates'}]
if (!type.value) {
return [{ route: { name: "templates" }, label: "Templates" }]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: type.value.name}]
return [
{ route: { name: "templates" }, label: "Templates" },
{ label: type.value.name },
]
})
const type = computed(() => templatesStore.types.get(route.params.slug))
useOpnSeoMeta({
title: () => {
if (!type.value) return 'Form Templates'
if (!type.value) return "Form Templates"
if (type.value.meta_title.length > 60) {
return type.value.meta_title
}
return type.value.meta_title
},
description: () => type.value ? type.value.meta_description: 'Our collection of beautiful templates to create your own forms!'
description: () =>
type.value
? type.value.meta_description
: "Our collection of beautiful templates to create your own forms!",
})
useHead({
titleTemplate: (titleChunk) => {
// Disable title template for longer titles
if (type.value
&& type.value.meta_title.length < 60
&& !type.value.meta_title.toLowerCase().includes('opnform')
if (
type.value &&
type.value.meta_title.length < 60 &&
!type.value.meta_title.toLowerCase().includes("opnform")
) {
return titleChunk ? `${titleChunk} - OpnForm` : 'Form Templates - OpnForm'
return titleChunk
? `${titleChunk} - OpnForm`
: "Form Templates - OpnForm"
}
return titleChunk ? titleChunk : 'Form Templates - OpnForm'
}
return titleChunk ? titleChunk : "Form Templates - OpnForm"
},
})
</script>
<style lang='scss'>
<style lang="scss">
.nf-text {
@apply space-y-4;
h2 {
@@ -116,4 +133,3 @@ useHead({
}
}
</style>

View File

@@ -5,27 +5,32 @@
<h1 class="sm:text-5xl">
Terms & Conditions
</h1>
<NotionPage :block-map="blockMap" :loading="loading" />
<NotionPage
:block-map="blockMap"
:loading="loading"
/>
</div>
</div>
<open-form-footer/>
<open-form-footer />
</div>
</template>
<script setup>
import {useNotionPagesStore} from "~/stores/notion_pages.js";
import {computed} from "vue";
import { useNotionPagesStore } from "~/stores/notion_pages.js"
import { computed } from "vue"
useOpnSeoMeta({
title: 'Terms & Conditions'
title: "Terms & Conditions",
})
defineRouteRules({
swr: 3600
swr: 3600,
})
const notionPageStore = useNotionPagesStore()
await notionPageStore.load('246420da2834480ca04047b0c5a00929')
await notionPageStore.load("246420da2834480ca04047b0c5a00929")
const loading = computed(() => notionPageStore.loading)
const blockMap = computed(() => notionPageStore.getByKey('246420da2834480ca04047b0c5a00929'))
const blockMap = computed(() =>
notionPageStore.getByKey("246420da2834480ca04047b0c5a00929"),
)
</script>