Merge branch 'migrate-to-nuxt' of https://github.com/JhumanJ/OpnForm into migrate-to-nuxt

This commit is contained in:
Julien Nahum 2024-01-05 11:12:29 +01:00
commit 8b92f24094
45 changed files with 624 additions and 614 deletions

View File

@ -44,6 +44,17 @@ export default {
components: {}, components: {},
setup() { setup() {
useOpnSeoMeta({
title: 'OpnForm',
description: 'Create beautiful forms for free. Unlimited fields, unlimited submissions. It\'s free and it takes less than 1 minute to create your first form.',
ogImage: '/img/social-preview.jpg',
})
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm';
}
})
const appStore = useAppStore() const appStore = useAppStore()
return { return {
@ -57,8 +68,6 @@ export default {
}, },
data: () => ({ data: () => ({
metaTitle: 'OpnForm',
metaDescription: 'Create beautiful forms for free. Unlimited fields, unlimited submissions. It\'s free and it takes less than 1 minute to create your first form.',
announcement: false, announcement: false,
alert: { alert: {
type: null, type: null,

View File

@ -203,7 +203,7 @@ export default {
return !this.appStore.navbarHidden return !this.appStore.navbarHidden
}, },
userOnboarded() { userOnboarded() {
return this.user && this.user.workspaces_count > 0 return this.user && this.user.has_forms === true
}, },
hasCrisp() { hasCrisp() {
return this.config.crispWebsiteId return this.config.crispWebsiteId

View File

@ -6,7 +6,7 @@
</p> </p>
</div> </div>
<div class="w-full sm:w-40 sm:ml-2 mt-2 sm:mt-0 shrink-0"> <div class="w-full sm:w-40 sm:ml-2 mt-2 sm:mt-0 shrink-0">
<v-button color="light-gray" class="w-full" @click="copyToClipboard(content)"> <v-button color="light-gray" class="w-full" @click="copyToClipboard">
<slot name="icon"> <slot name="icon">
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 20 20" fill="none" <svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 20 20" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -21,47 +21,28 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import { defineProps } from 'vue'
name: 'CopyContent', const { copy } = useClipboard()
props: {
content: { const props = defineProps({
type: String, content: {
required: true type: String,
}, required: true
isDraft: {
type: Boolean,
default: false
},
}, },
isDraft: {
type: Boolean,
default: false
}
})
data() { const copyToClipboard = () => {
return {} if (process.server) return
}, copy(props.content)
if(props.isDraft){
computed: {}, useAlert().warning('Copied! But other people won\'t be able to see the form since it\'s currently in draft mode')
} else {
watch: {}, useAlert().success('Copied!')
mounted() {
},
methods: {
copyToClipboard(str) {
if (process.server) return
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
if(this.isDraft){
useAlert().warning('Copied! But other people won\'t be able to see the form since it\'s currently in draft mode')
} else {
useAlert().success('Copied!')
}
}
} }
} }
</script> </script>

View File

@ -17,72 +17,50 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import { defineProps, computed } from 'vue'
name: 'FormUrlPrefill', const { copy } = useClipboard()
props: {
form: { const props = defineProps({
type: Object, form: {
required: true type: Object,
}, required: true
formData: {
type: Object,
required: true
},
extraQueryParam: {
type: String,
default: ''
}
}, },
formData: {
data () { type: Object,
return {} required: true
}, },
extraQueryParam: {
computed: { type: String,
preFillUrl () { default: ''
const url = this.form.share_url
const uriComponents = new URLSearchParams()
this.form.properties.filter((property) => {
return this.formData.hasOwnProperty(property.id) && this.formData[property.id] !== null
}).forEach((property) => {
if (Array.isArray(this.formData[property.id])) {
this.formData[property.id].forEach((value) => {
uriComponents.append(property.id + '[]', value)
})
} else {
uriComponents.append(property.id, this.formData[property.id])
}
})
if(uriComponents.toString() !== ""){
return (this.extraQueryParam) ? url + '?' + uriComponents + '&' + this.extraQueryParam : url + '?' + uriComponents
}else{
return (this.extraQueryParam) ? url + '?' + this.extraQueryParam : url
}
}
},
watch: {},
mounted () {
},
methods: {
getPropertyUriComponent (property) {
const prefillValue = encodeURIComponent(this.formData[property.id])
return encodeURIComponent(property.id) + '=' + prefillValue
},
copyToClipboard () {
if (process.server) return
const str = this.preFillUrl
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
} }
})
const preFillUrl = computed(() => {
const url = props.form.share_url
const uriComponents = new URLSearchParams()
props.form.properties.filter((property) => {
return props.formData.hasOwnProperty(property.id) && props.formData[property.id] !== null
}).forEach((property) => {
if (Array.isArray(props.formData[property.id])) {
props.formData[property.id].forEach((value) => {
uriComponents.append(property.id + '[]', value)
})
} else {
uriComponents.append(property.id, props.formData[property.id])
}
})
if(uriComponents.toString() !== ""){
return (props.extraQueryParam) ? url + '?' + uriComponents + '&' + props.extraQueryParam : url + '?' + uriComponents
}else{
return (props.extraQueryParam) ? url + '?' + props.extraQueryParam : url
}
})
const copyToClipboard = () => {
if (process.server) return
copy(preFillUrl.value)
useAlert().success('Copied!')
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<modal :show="show" @close="$emit('close')"> <modal :show="show" @close="emit('close')">
<template #icon> <template #icon>
<svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -57,11 +57,11 @@
</template> </template>
</v-button> </v-button>
<v-button v-if="template" color="red" class="mr-2" <v-button v-if="template" color="red" class="mr-2"
@click.prevent="alertConfirm('Do you really want to delete this template?', deleteFormTemplate)" @click.prevent="useAlert().confirm('Do you really want to delete this template?', deleteFormTemplate)"
> >
Delete Delete
</v-button> </v-button>
<v-button color="white" @click.prevent="$emit('close')"> <v-button color="white" @click.prevent="emit('close')">
Close Close
</v-button> </v-button>
</div> </div>
@ -71,118 +71,104 @@
</modal> </modal>
</template> </template>
<script> <script setup>
import { computed } from 'vue' import { ref, defineProps, defineEmits, computed } from 'vue'
import QuestionsEditor from './QuestionsEditor.vue' import QuestionsEditor from './QuestionsEditor.vue'
export default { const props = defineProps({
name: 'FormTemplateModal', show: { type: Boolean, required: true },
components: { QuestionsEditor }, form: { type: Object, required: true },
props: { template: { type: Object, required: false, default: () => {} }
show: { type: Boolean, required: true }, })
form: { type: Object, required: true },
template: { type: Object, required: false, default: () => {} }
},
setup () { const authStore = useAuthStore()
const authStore = useAuthStore() const templatesStore = useTemplatesStore()
const templatesStore = useTemplatesStore() const router = useRouter()
let user = computed(() => authStore.user)
let templates = computed(() => [...templatesStore.content.values()])
let industries = computed(() => [...templatesStore.industries.values()])
let types = computed(() => [...templatesStore.types.values()])
let templateForm = ref(null)
const emit = defineEmits(['close'])
onMounted(() => {
templateForm.value = useForm(props.template ?? {
publicly_listed: false,
name: '',
slug: '',
short_description: '',
description: '',
image_url: '',
types: null,
industries: null,
related_templates: null,
questions: []
})
loadAllTemplates(templatesStore)
})
let typesOptions = computed(() => {
return Object.values(types.value).map((type) => {
return { return {
templatesStore, name: type.name,
user : computed(() => authStore.user), value: type.slug
templates : computed(() => templatesStore.content),
industries : computed(() => templatesStore.industries),
types : computed(() => templatesStore.types),
useAlert: useAlert()
} }
}, })
})
data: () => ({ let industriesOptions = computed(() => {
templateForm: null return Object.values(industries.value).map((industry) => {
}), return {
name: industry.name,
mounted () { value: industry.slug
this.templateForm = useForm(this.template ?? {
publicly_listed: false,
name: '',
slug: '',
short_description: '',
description: '',
image_url: '',
types: null,
industries: null,
related_templates: null,
questions: []
})
loadAllTemplates(this.templatesStore)
},
computed: {
typesOptions () {
return Object.values(this.types).map((type) => {
return {
name: type.name,
value: type.slug
}
})
},
industriesOptions () {
return Object.values(this.industries).map((industry) => {
return {
name: industry.name,
value: industry.slug
}
})
},
templatesOptions () {
return this.templates.map((template) => {
return {
name: template.name,
value: template.slug
}
})
} }
}, })
})
methods: { let templatesOptions = computed(() => {
onSubmit () { return Object.values(templates.value).map((template) => {
if (this.template) { return {
this.updateFormTemplate() name: template.name,
} else { value: template.slug
this.createFormTemplate()
}
},
async createFormTemplate () {
this.templateForm.form = this.form
await this.templateForm.post('/api/templates').then((response) => {
if (response.data.message) {
this.useAlert.success(response.data.message)
}
this.templatesStore.save(response.data.data)
this.$emit('close')
})
},
async updateFormTemplate () {
this.templateForm.form = this.form
await this.templateForm.put('/api/templates/' + this.template.id).then((response) => {
if (response.data.message) {
this.useAlert.success(response.data.message)
}
this.templatesStore.save(response.data.data)
this.$emit('close')
})
},
async deleteFormTemplate () {
if (!this.template) return
opnFetch('/templates/' + this.template.id, {method:'DELETE'}).then((data) => {
if (data.message) {
this.useAlert.success(data.message)
}
this.$router.push({ name: 'templates' })
this.templatesStore.remove(this.template)
this.$emit('close')
})
} }
})
})
const onSubmit = () => {
if (props.template) {
updateFormTemplate()
} else {
createFormTemplate()
} }
} }
const createFormTemplate = async () => {
templateForm.value.form = props.form
await templateForm.value.post('/templates').then((data) => {
if (data.message) {
useAlert().success(data.message)
}
templatesStore.save(data.data)
emit('close')
})
}
const updateFormTemplate = async () => {
templateForm.value.form = props.form
await templateForm.value.put('/templates/' + props.template.id).then((data) => {
if (data.message) {
useAlert().success(data.message)
}
templatesStore.save(data.data)
emit('close')
})
}
const deleteFormTemplate = async () => {
if (!props.template) return
opnFetch('/templates/' + props.template.id, {method:'DELETE'}).then((data) => {
if (data.message) {
useAlert().success(data.message)
}
router.push({ name: 'templates' })
templatesStore.remove(props.template)
emit('close')
})
}
</script> </script>

View File

@ -59,7 +59,7 @@
</div> </div>
</div> </div>
<collapse class="py-5 w-full border rounded-md px-4" :model-value="false"> <collapse class="py-5 w-full border rounded-md px-4" :model-value="true">
<template #title> <template #title>
<div class="flex"> <div class="flex">
<h3 class="font-semibold block text-lg"> <h3 class="font-semibold block text-lg">
@ -101,99 +101,87 @@
</div> </div>
</template> </template>
<script> <script setup>
import Collapse from '~/components/global/Collapse.vue' import { ref, defineProps, computed } from 'vue'
export default { const { copy } = useClipboard()
name: 'EmbedFormAsPopupModal', const crisp = useCrisp()
components: { Collapse }, const props = defineProps({
props: { form: { type: Object, required: true }
form: { type: Object, required: true } })
},
data: () => ({ const embedScriptUrl = '/widgets/embed-min.js'
showEmbedFormAsPopupModal: false, let showEmbedFormAsPopupModal = ref(false)
embedScriptUrl: 'widgets/embed-min.js', let advancedOptions = ref({
advancedOptions: { hide_title: false,
hide_title: false, emoji: '💬',
emoji: '💬', position: 'right',
position: 'right', bgcolor: '#3B82F6',
bgcolor: '#3B82F6', width: '500'
width: '500' })
}
}),
computed: { let hideTitleHelp = computed(() => {
hideTitleHelp () { return props.form.hide_title ? 'This option is disabled because the form title is already hidden' : null
return this.form.hide_title ? 'This option is disabled because the form title is already hidden' : null })
}, let shareUrl = computed(() => {
shareUrl () { return (advancedOptions.value.hide_title) ? props.form.share_url + '?hide_title=true' : props.form.share_url
return (this.advancedOptions.hide_title) ? this.form.share_url + '?hide_title=true' : this.form.share_url })
}, let embedPopupCode = computed(() => {
embedPopupCode () { const nfData = {
const nfData = { formurl: shareUrl.value,
formurl: this.shareUrl, emoji: advancedOptions.value.emoji,
emoji: this.advancedOptions.emoji, position: advancedOptions.value.position,
position: this.advancedOptions.position, bgcolor: advancedOptions.value.bgcolor,
bgcolor: this.advancedOptions.bgcolor, width: advancedOptions.value.width
width: this.advancedOptions.width }
} previewPopup(nfData)
this.previewPopup(nfData) return '<script async data-nf=\'' + JSON.stringify(nfData) + '\' src=\'' + embedScriptUrl + '\'></scrip' + 't>'
return '<script async data-nf=\'' + JSON.stringify(nfData) + '\' src=\'' + this.asset(this.embedScriptUrl) + '\'></scrip' + 't>' })
}
},
mounted () { onMounted(() => {
this.advancedOptions.bgcolor = this.form.color advancedOptions.value.bgcolor = props.form.color
}, })
methods: {
onClose () {
this.removePreview()
this.$crisp.push(['do', 'chat:show'])
this.showEmbedFormAsPopupModal = false
},
copyToClipboard () {
if (process.server) return
const str = this.embedPopupCode
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
},
removePreview () {
if (process.server) return
const oldP = document.head.querySelector('#nf-popup-preview')
if (oldP) {
oldP.remove()
}
const oldM = document.body.querySelector('.nf-main')
if (oldM) {
oldM.remove()
}
},
previewPopup (nfData) {
if (process.server) return
if (!this.showEmbedFormAsPopupModal) {
return
}
// Remove old preview, if there const onClose = () => {
this.removePreview() removePreview()
crisp.showChat()
// Hide crisp showEmbedFormAsPopupModal.value = false
this.$crisp.push(['do', 'chat:hide']) }
const copyToClipboard = () => {
// Add new preview if (process.server) return
const el = document.createElement('script') copy(embedPopupCode.value)
el.id = 'nf-popup-preview' useAlert().success('Copied!')
el.async = true }
el.src = this.asset(this.embedScriptUrl) const removePreview = () => {
el.setAttribute('data-nf', JSON.stringify(nfData)) if (process.server) return
document.head.appendChild(el) const oldP = document.head.querySelector('#nf-popup-preview')
} if (oldP) {
oldP.remove()
}
const oldM = document.body.querySelector('.nf-main')
if (oldM) {
oldM.remove()
} }
} }
const previewPopup = (nfData) => {
if (process.server) return
if (!showEmbedFormAsPopupModal.value) {
return
}
// Remove old preview, if there
removePreview()
// Hide crisp
crisp.hideChat()
// Add new preview
const el = document.createElement('script')
el.id = 'nf-popup-preview'
el.async = true
el.src = embedScriptUrl
el.setAttribute('data-nf', JSON.stringify(nfData))
document.head.appendChild(el)
}
</script> </script>

View File

@ -138,70 +138,51 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed } from 'vue' import { ref, defineProps, computed } from 'vue'
import Dropdown from '~/components/global/Dropdown.vue' import Dropdown from '~/components/global/Dropdown.vue'
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue' import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'
export default { const { copy } = useClipboard()
name: 'ExtraMenu', const router = useRouter()
components: { Dropdown, FormTemplateModal },
props: {
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
},
setup () { const props = defineProps({
const authStore = useAuthStore() form: { type: Object, required: true },
const formsStore = useFormsStore() isMainPage: { type: Boolean, required: false, default: false }
return { })
formsStore,
user: computed(() => authStore.user),
useAlert: useAlert()
}
},
data: () => ({ const authStore = useAuthStore()
loadingDuplicate: false, const formsStore = useFormsStore()
loadingDelete: false, const formEndpoint = '/open/forms/{id}'
showDeleteFormModal: false, let user = computed(() => authStore.user)
showFormTemplateModal: false
}),
computed: { let loadingDuplicate = ref(false)
formEndpoint: () => '/open/forms/{id}' let loadingDelete = ref(false)
}, let showDeleteFormModal = ref(false)
let showFormTemplateModal = ref(false)
methods: { const copyLink = () => {
copyLink () { copy(props.form.share_url)
const el = document.createElement('textarea') useAlert().success('Copied!')
el.value = this.form.share_url }
document.body.appendChild(el) const duplicateForm = () => {
el.select() if (loadingDuplicate.value) return
document.execCommand('copy') loadingDuplicate.value = true
document.body.removeChild(el) opnFetch(formEndpoint.replace('{id}', props.form.id) + '/duplicate',{method: 'POST'}).then((data) => {
this.useAlert.success('Copied!') formsStore.save(data.new_form)
}, router.push({ name: 'forms-slug-show', params: { slug: data.new_form.slug } })
duplicateForm () { useAlert().success('Form was successfully duplicated.')
if (this.loadingDuplicate) return loadingDuplicate.value = false
this.loadingDuplicate = true })
opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate',{method: 'POST'}).then((data) => { }
this.formsStore.save(data.new_form) const deleteForm = () => {
this.$router.push({ name: 'forms-show', params: { slug: data.new_form.slug } }) if (loadingDelete.value) return
this.useAlert.success('Form was successfully duplicated.') loadingDelete.value = true
this.loadingDuplicate = false opnFetch(formEndpoint.replace('{id}', props.form.id),{method:'DELETE'}).then(() => {
}) formsStore.remove(props.form)
}, router.push({ name: 'home' })
deleteForm () { useAlert().success('Form was deleted.')
if (this.loadingDelete) return loadingDelete.value = false
this.loadingDelete = true })
opnFetch(this.formEndpoint.replace('{id}', this.form.id),{method:'DELETE'}).then(() => {
this.formsStore.remove(this.form)
this.$router.push({ name: 'home' })
this.useAlert.success('Form was deleted.')
this.loadingDelete = false
})
}
}
} }
</script> </script>

16
client/composables/useOpnSeoMeta.js vendored Normal file
View File

@ -0,0 +1,16 @@
export const useOpnSeoMeta = (meta) => {
return useSeoMeta({
...meta.title ? {
ogTitle: meta.title,
twitterTitle: meta.title,
} : {},
...meta.description ? {
ogDescription: meta.description,
twitterDescription: meta.description,
} : {},
...meta.ogImage ? {
twitterImage: meta.ogImage,
} : {},
...meta,
})
}

25
client/error.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<NuxtLayout>
<div class="flex mt-6">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md">
<img alt="Nice plant as we have nothing else to show!" src="/img/icons/plant.png" class="w-56 mb-5">
<h1 class="mb-4 font-semibold text-3xl text-gray-900">
Page Not Found
</h1>
<div class="links">
<NuxtLink :to="{ name: 'index' }" class="hover:underline text-gray-700">
Go Home
</NuxtLink>
</div>
</div>
</div>
</NuxtLayout>
</template>
<script setup>
useOpnSeoMeta({
title: '404 - Page not found'
})
</script>

View File

@ -1,23 +0,0 @@
export default {
metaInfo () {
const title = this.metaTitle ?? 'OpnForm'
const description = this.metaDescription ?? "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form."
const image = this.metaImage ?? this.asset('img/social-preview.jpg')
const metaTemplate = this.metaTemplate ?? '%s · OpnForm'
return {
title: title,
titleTemplate: metaTemplate,
meta: [
...(this.metaTags ?? []),
{ vmid: 'og:title', property: 'og:title', content: title },
{ vmid: 'twitter:title', property: 'twitter:title', content: title },
{ vmid: 'description', name: 'description', content: description },
{ vmid: 'og:description', property: 'og:description', content: description },
{ vmid: 'twitter:description', property: 'twitter:description', content: description },
{ vmid: 'twitter:image', property: 'twitter:image', content: image },
{ vmid: 'og:image', property: 'og:image', content: image }
]
}
}
}

View File

@ -493,39 +493,21 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import SeoMeta from '../mixins/seo-meta.js'
export default { const authStore = useAuthStore()
layout: 'default',
mixins: [SeoMeta],
setup () { useOpnSeoMeta({
const authStore = useAuthStore() title: 'Free AI form builder',
defineRouteRules({ description: 'Transform your ideas into fully functional forms with OpnForm AI Builder quick, accurate, and tailored to fit any requirement.'
prerender: true })
}) defineRouteRules({
prerender: true
})
return { let authenticated = computed(() => authStore.check)
authenticated : computed(() => authStore.check),
}
},
data: () => ({
title: 'OpnForm',
metaTitle: 'AI form builder for free',
}),
mounted() {},
methods: {},
computed: {
configLinks: () => this.$config.links
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -24,10 +24,16 @@
<script> <script>
export default { export default {
middleware: 'guest', setup () {
definePageMeta({
middleware: "guest"
})
useOpnSeoMeta({
title: 'Reset Password'
})
},
data: () => ({ data: () => ({
metaTitle: 'Reset Password',
status: '', status: '',
form: useForm({ form: useForm({
email: '' email: ''

View File

@ -34,10 +34,16 @@
<script> <script>
export default { export default {
middleware: 'guest', setup () {
definePageMeta({
middleware: "guest"
})
useOpnSeoMeta({
title: 'Reset Password'
})
},
data: () => ({ data: () => ({
metaTitle: 'Reset Password',
status: '', status: '',
form: useForm({ form: useForm({
token: '', token: '',

View File

@ -1,24 +0,0 @@
<template>
<div class="flex mt-6">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md">
<img alt="Nice plant as we have nothing else to show!" src="/img/icons/plant.png" class="w-56 mb-5">
<h1 class="mb-4 font-semibold text-3xl text-gray-900">
Page Not Found
</h1>
<div class="links">
<NuxtLink :to="{ name: 'index' }" class="hover:underline text-gray-700">
Go Home
</NuxtLink>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>

View File

@ -22,7 +22,6 @@ import {hash} from "~/lib/utils.js";
export default { export default {
name: 'EditForm', name: 'EditForm',
components: { Breadcrumb, FormEditor }, components: { Breadcrumb, FormEditor },
middleware: 'auth',
beforeRouteLeave (to, from, next) { beforeRouteLeave (to, from, next) {
if (this.isDirty()) { if (this.isDirty()) {
@ -52,6 +51,13 @@ export default {
} }
}) })
useOpnSeoMeta({
title: 'Edit ' + ((form && form.value) ? form.value.title : 'Your Form')
})
definePageMeta({
middleware: "auth"
})
return { return {
formsStore, formsStore,
workingFormStore, workingFormStore,
@ -70,9 +76,6 @@ export default {
}, },
computed: { computed: {
metaTitle () {
return 'Edit ' + (this.form ? this.form.title : 'Your Form')
}
}, },
async beforeMount() { async beforeMount() {

View File

@ -113,32 +113,36 @@ onMounted(() => {
await loadForm(slug) await loadForm(slug)
// metaTitle () { useOpnSeoMeta({
// if (this.form && this.form.is_pro && this.form.seo_meta.page_title) { title: () => {
// return this.form.seo_meta.page_title if (form && form.value.is_pro && form.value.seo_meta.page_title) {
// } return form.value.seo_meta.page_title
// return this.form ? this.form.title : 'Create beautiful forms' }
// }, return form.value ? form.value.title : 'Create beautiful forms'
// metaTemplate () { },
// if (this.form && this.form.is_pro && this.form.seo_meta.page_title) { description () {
// // Disable template if custom SEO title if (form && form.value.is_pro && form.value.seo_meta.page_description) {
// return '%s' return form.value.seo_meta.page_description
// } }
// return null return (form && form.value.description) ? form.value.description.substring(0, 160) : null
// }, },
// metaDescription () { ogImage () {
// if (this.form && this.form.is_pro && this.form.seo_meta.page_description) { if (form && form.value.is_pro && form.value.seo_meta.page_thumbnail) {
// return this.form.seo_meta.page_description return form.value.seo_meta.page_thumbnail
// } }
// return (this.form && this.form.description) ? this.form.description.substring(0, 160) : null return (form && form.value.cover_picture) ? form.value.cover_picture : null
// }, },
// metaImage () { robots () {
// if (this.form && this.form.is_pro && this.form.seo_meta.page_thumbnail) { return (form && form.value.can_be_indexed) ? null : 'noindex, nofollow'
// return this.form.seo_meta.page_thumbnail }
// } })
// return (this.form && this.form.cover_picture) ? this.form.cover_picture : null useHead({
// }, titleTemplate: (titleChunk) => {
// metaTags () { if (form && form.value.is_pro && form.value.seo_meta.page_title) {
// return (this.form && this.form.can_be_indexed) ? [] : [{ name: 'robots', content: 'noindex' }] // Disable template if custom SEO title
// } return titleChunk
}
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm';
}
})
</script> </script>

View File

@ -141,9 +141,14 @@ export default {
FormCleanings FormCleanings
}, },
middleware: 'auth',
setup () { setup () {
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: 'Home'
})
const authStore = useAuthStore() const authStore = useAuthStore()
const formsStore = useFormsStore() const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore() const workingFormStore = useWorkingFormStore()
@ -167,7 +172,6 @@ export default {
data () { data () {
return { return {
metaTitle: 'Home',
tabsList: [ tabsList: [
{ {
name: 'Submissions', name: 'Submissions',

View File

@ -26,7 +26,6 @@ import EmbedCode from '../../../../components/pages/forms/show/EmbedCode.vue'
import FormQrCode from '../../../../components/pages/forms/show/FormQrCode.vue' import FormQrCode from '../../../../components/pages/forms/show/FormQrCode.vue'
import UrlFormPrefill from '../../../../components/pages/forms/show/UrlFormPrefill.vue' import UrlFormPrefill from '../../../../components/pages/forms/show/UrlFormPrefill.vue'
import RegenerateFormLink from '../../../../components/pages/forms/show/RegenerateFormLink.vue' import RegenerateFormLink from '../../../../components/pages/forms/show/RegenerateFormLink.vue'
import SeoMeta from '../../../../mixins/seo-meta.js'
import AdvancedFormUrlSettings from '../../../../components/open/forms/components/AdvancedFormUrlSettings.vue' import AdvancedFormUrlSettings from '../../../../components/open/forms/components/AdvancedFormUrlSettings.vue'
import EmbedFormAsPopupModal from '../../../../components/pages/forms/show/EmbedFormAsPopupModal.vue' import EmbedFormAsPopupModal from '../../../../components/pages/forms/show/EmbedFormAsPopupModal.vue'
@ -45,6 +44,15 @@ export default {
form: {type: Object, required: true}, form: {type: Object, required: true},
}, },
setup (props) {
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Share Form - '+props.form.title : 'Share Form'
})
},
data: () => ({ data: () => ({
shareFormConfig: { shareFormConfig: {
hide_title: false, hide_title: false,
@ -53,9 +61,6 @@ export default {
}), }),
computed: { computed: {
metaTitle() {
return (this.form) ? 'Form Share - '+this.form.title : 'Form Share'
},
shareUrlForQueryParams () { shareUrlForQueryParams () {
let queryStr = '' let queryStr = ''
for (const [key, value] of Object.entries(this.shareFormConfig)) { for (const [key, value] of Object.entries(this.shareFormConfig)) {

View File

@ -9,7 +9,6 @@
<script> <script>
import FormStats from '../../../../components/open/forms/components/FormStats.vue' import FormStats from '../../../../components/open/forms/components/FormStats.vue'
import SeoMeta from '../../../../mixins/seo-meta.js'
export default { export default {
components: {FormStats}, components: {FormStats},
@ -18,10 +17,16 @@ export default {
form: {type: Object, required: true}, form: {type: Object, required: true},
}, },
setup (props) {
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Form Analytics - '+props.form.title : 'Form Analytics'
})
},
computed: { computed: {
metaTitle() {
return (this.form ? ('Form Analytics - ' + this.form.title) : 'Form Analytics')
}
} }
} }
</script> </script>

View File

@ -6,14 +6,21 @@
<script> <script>
import FormSubmissions from '../../../../components/open/forms/components/FormSubmissions.vue' import FormSubmissions from '../../../../components/open/forms/components/FormSubmissions.vue'
import SeoMeta from '../../../../mixins/seo-meta.js'
export default { export default {
components: {FormSubmissions}, components: {FormSubmissions},
props: { props: {
form: {type: Object, required: true} form: {type: Object, required: true}
}, },
mixins: [SeoMeta],
setup (props) {
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Form Submissions - '+props.form.title : 'Form Submissions'
})
},
data: () => ({}), data: () => ({}),
@ -21,9 +28,6 @@ export default {
}, },
computed: { computed: {
metaTitle() {
return (this.form) ? 'Form Submissions - ' + this.form.title : 'Form Submissions'
},
}, },
methods: {} methods: {}

View File

@ -48,7 +48,13 @@ const workspace = computed(() => workspacesStore.getCurrent)
const workspacesLoading = computed(() => workspacesStore.loading) const workspacesLoading = computed(() => workspacesStore.loading)
const form = storeToRefs(workingFormStore).content const form = storeToRefs(workingFormStore).content
// metaTitle: 'Create a new Form as Guest', useOpnSeoMeta({
title: 'Create a new Form for free',
})
definePageMeta({
middleware: "guest"
})
// Data // Data
const stateReady = ref(false) const stateReady = ref(false)
const loading = ref(false) const loading = ref(false)

View File

@ -33,7 +33,9 @@ definePageMeta({
middleware: "auth" middleware: "auth"
}) })
const metaTitle = 'Create a new Form' useOpnSeoMeta({
title: 'Create a new Form'
})
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (isDirty()) { if (isDirty()) {

View File

@ -125,11 +125,10 @@ definePageMeta({
middleware: "auth" middleware: "auth"
}) })
// metaTitle: {type: String, default: 'Your Forms'}, useOpnSeoMeta({
// metaDescription: { title: 'Your Forms',
// type: String, description: 'All of your OpnForm are here. Create new forms, or update your existing forms.'
// default: 'All of your OpnForm are here. Create new forms, or update your existing one!' })
// }
const authStore = useAuthStore() const authStore = useAuthStore()
const formsStore = useFormsStore() const formsStore = useFormsStore()

View File

@ -191,12 +191,10 @@ import PricingTable from '../components/pages/pricing/PricingTable.vue'
import AiFeature from '~/components/pages/welcome/AiFeature.vue' import AiFeature from '~/components/pages/welcome/AiFeature.vue'
import Testimonials from '../components/pages/welcome/Testimonials.vue' import Testimonials from '../components/pages/welcome/Testimonials.vue'
import TemplatesSlider from '../components/pages/welcome/TemplatesSlider.vue' import TemplatesSlider from '../components/pages/welcome/TemplatesSlider.vue'
import SeoMeta from '../mixins/seo-meta.js'
import opnformConfig from "~/opnform.config.js"; import opnformConfig from "~/opnform.config.js";
export default { export default {
components: {Testimonials, Features, MoreFeatures, PricingTable, AiFeature, TemplatesSlider}, components: {Testimonials, Features, MoreFeatures, PricingTable, AiFeature, TemplatesSlider},
mixins: [SeoMeta],
layout: 'default', layout: 'default',
setup() { setup() {
@ -213,8 +211,6 @@ export default {
}, },
data: () => ({ data: () => ({
title: 'OpnForm',
metaTitle: 'Create beautiful & open-source forms for free'
}), }),
computed: { computed: {

View File

@ -52,29 +52,16 @@
</div> </div>
</template> </template>
<script> <script setup>
import LoginForm from "~/components/pages/auth/components/LoginForm.vue" import LoginForm from "~/components/pages/auth/components/LoginForm.vue"
export default { definePageMeta({
components: { middleware: "guest"
LoginForm })
}, defineRouteRules({
prerender: true
setup() { })
definePageMeta({ useOpnSeoMeta({
middleware: "guest" title: 'Login'
}) })
defineRouteRules({
prerender: true
})
},
data: () => ({
metaTitle: 'Login',
}),
methods: {
}
}
</script> </script>

View File

@ -245,6 +245,11 @@ export default {
layout: 'default', 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.'
})
definePageMeta({ definePageMeta({
middleware: [ middleware: [
function (to, from) { function (to, from) {
@ -264,15 +269,6 @@ export default {
} }
}, },
data: () => ({
metaTitle: 'Pricing',
metaDescription: '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.',
}),
mounted() {},
computed: {},
methods: { methods: {
contactUs() { contactUs() {
window.$crisp.push(['do', 'chat:show']) window.$crisp.push(['do', 'chat:show'])

View File

@ -13,12 +13,13 @@
</template> </template>
<script setup> <script setup>
// metaTitle: 'Privacy Policy',
import {useNotionPagesStore} from "~/stores/notion_pages.js"; import {useNotionPagesStore} from "~/stores/notion_pages.js";
import {computed} from "vue"; import {computed} from "vue";
useOpnSeoMeta({
title: 'Privacy Policy'
})
const notionPageStore = useNotionPagesStore() const notionPageStore = useNotionPagesStore()
await notionPageStore.load('9c97349ceda7455aab9b341d1ff70f79') await notionPageStore.load('9c97349ceda7455aab9b341d1ff70f79')

View File

@ -64,6 +64,10 @@ export default {
}, },
setup() { setup() {
useOpnSeoMeta({
title: 'Register'
})
definePageMeta({ definePageMeta({
middleware: "guest" middleware: "guest"
}) })
@ -72,10 +76,7 @@ export default {
}) })
}, },
middleware: 'guest',
data: () => ({ data: () => ({
metaTitle: 'Register'
}), }),
computed: {}, computed: {},

View File

@ -11,7 +11,7 @@
<ul class="flex text-gray-500"> <ul class="flex text-gray-500">
<li>{{ user.email }}</li> <li>{{ user.email }}</li>
</ul> </ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700"> <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"> <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"> <li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
@ -36,15 +36,19 @@
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
definePageMeta({ definePageMeta({
middleware: "auth" middleware: "auth"
}) })
useOpnSeoMeta({
title: 'Settings'
})
const authStore = useAuthStore() const authStore = useAuthStore()
const user = computed(() => authStore.user) const user = computed(() => authStore.user)
const tabsList = computed(() => { const tabsList = computed(() => {
@ -66,22 +70,21 @@
route: 'settings-account' route: 'settings-account'
} }
] ]
if (user.value.is_subscribed) { if (user.value.is_subscribed) {
tabs.splice(1, 0, { tabs.splice(1, 0, {
name: 'Billing', name: 'Billing',
route: 'settings-billing' route: 'settings-billing'
}) })
} }
if (user.value.admin) { if (user.value.admin) {
tabs.push({ tabs.push({
name: 'Admin', name: 'Admin',
route: 'settings-admin' route: 'settings-admin'
}) })
} }
return tabs return tabs
}) })
</script> </script>

View File

@ -20,15 +20,21 @@ import { useRouter } from 'vue-router';
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
const metaTitle = 'Account'
let loading = false let loading = false
useOpnSeoMeta({
title: 'Account'
})
definePageMeta({
middleware: "auth"
})
const deleteAccount = () => { const deleteAccount = () => {
loading = true loading = true
opnFetch('/user', {method:'DELETE'}).then(async (data) => { opnFetch('/user', {method:'DELETE'}).then(async (data) => {
loading = false loading = false
useAlert().success(data.message) useAlert().success(data.message)
// Log out the user. // Log out the user.
await authStore.logout() await authStore.logout()

View File

@ -41,7 +41,10 @@ definePageMeta({
middleware: "admin" middleware: "admin"
}) })
const metaTitle = 'Admin' useOpnSeoMeta({
title: 'Admin'
})
const authStore = useAuthStore() const authStore = useAuthStore()
const workspacesStore = useWorkspacesStore() const workspacesStore = useWorkspacesStore()
const router = useRouter() const router = useRouter()

View File

@ -24,7 +24,13 @@ import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue' import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue'
const metaTitle = 'Billing' useOpnSeoMeta({
title: 'Billing'
})
definePageMeta({
middleware: "auth"
})
const authStore = useAuthStore() const authStore = useAuthStore()
let user = computed(() => authStore.user) let user = computed(() => authStore.user)
let billingLoading = false let billingLoading = false

View File

@ -25,7 +25,13 @@
</template> </template>
<script setup> <script setup>
const metaTitle = 'Password' useOpnSeoMeta({
title: 'Password'
})
definePageMeta({
middleware: "auth"
})
let form = useForm({ let form = useForm({
password: '', password: '',
password_confirmation: '' password_confirmation: ''

View File

@ -23,7 +23,14 @@
<script setup> <script setup>
const authStore = useAuthStore() const authStore = useAuthStore()
const user = computed(() => authStore.user) const user = computed(() => authStore.user)
const metaTitle = 'Profile'
useOpnSeoMeta({
title: 'Profile'
})
definePageMeta({
middleware: "auth"
})
let form = useForm({ let form = useForm({
name: '', name: '',
email: '' email: ''

View File

@ -122,7 +122,14 @@ const crisp = useCrisp()
const workspacesStore = useWorkspacesStore() const workspacesStore = useWorkspacesStore()
const workspaces = computed(() => workspacesStore.getAll) const workspaces = computed(() => workspacesStore.getAll)
let loading = computed(() => workspacesStore.loading) let loading = computed(() => workspacesStore.loading)
const metaTitle = 'Workspaces'
useOpnSeoMeta({
title: 'Workspaces'
})
definePageMeta({
middleware: "auth"
})
let form = useForm({ let form = useForm({
name: '', name: '',
emoji: '' emoji: ''

View File

@ -3,15 +3,17 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import SeoMeta from '../../mixins/seo-meta.js'
export default { export default {
components: { }, components: { },
layout: 'default', layout: 'default',
middleware: 'auth', middleware: 'auth',
mixins: [SeoMeta],
setup () { setup () {
useOpnSeoMeta({
title: 'Error'
})
const authStore = useAuthStore() const authStore = useAuthStore()
return { return {
authenticated : computed(() => authStore.check), authenticated : computed(() => authStore.check),
@ -19,7 +21,6 @@ export default {
}, },
data: () => ({ data: () => ({
metaTitle: 'Error',
}), }),
mounted () { mounted () {

View File

@ -18,13 +18,16 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import SeoMeta from '../../mixins/seo-meta.js'
export default { export default {
layout: 'default', layout: 'default',
middleware: 'auth', middleware: 'auth',
setup () { setup () {
useOpnSeoMeta({
title: 'Subscription Success'
})
const authStore = useAuthStore() const authStore = useAuthStore()
return { return {
authStore, authStore,
@ -34,7 +37,6 @@ export default {
}, },
data: () => ({ data: () => ({
metaTitle: 'Subscription Success',
interval: null interval: null
}), }),

View File

@ -6,9 +6,9 @@
<v-button color="gray" size="small" @click.prevent="showFormTemplateModal=true"> <v-button color="gray" size="small" @click.prevent="showFormTemplateModal=true">
Edit Template Edit Template
</v-button> </v-button>
<!-- <form-template-modal v-if="form" :form="form" :template="template" :show="showFormTemplateModal"--> <form-template-modal v-if="form" :form="form" :template="template" :show="showFormTemplateModal"
<!-- @close="showFormTemplateModal=false"--> @close="showFormTemplateModal=false"
<!-- />--> />
</div> </div>
</template> </template>
<template #right> <template #right>
@ -199,12 +199,14 @@ import {computed} from 'vue'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue' import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue' import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue' import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
import {fetchTemplate} from "~/stores/templates.js"; import {fetchTemplate} from "~/stores/templates.js"
import FormTemplateModal from '~/components/open/forms/components/templates/FormTemplateModal.vue'
defineRouteRules({ defineRouteRules({
prerender: true prerender: true
}) })
const { copy } = useClipboard()
const authStore = useAuthStore() const authStore = useAuthStore()
const templatesStore = useTemplatesStore() const templatesStore = useTemplatesStore()
@ -255,34 +257,29 @@ const cleanQuotes = (str) => {
} }
const copyTemplateUrl = () => { const copyTemplateUrl = () => {
const str = template.value.share_url copy(template.value.share_url)
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
useAlert().success('Copied!') useAlert().success('Copied!')
} }
// metaTitle() { useOpnSeoMeta({
// return this.template ? this.template.name : 'Form Template' title: () => {
// }, if (!template || !template.value) return 'Form Template'
// metaDescription() { return template.value.name
// if (!this.template) return null },
// // take the first 140 characters of the description description () {
// return this.template.short_description?.substring(0, 140) + '... | Customize any template and create your own form in minutes.' if (!template || !template.value) return null
// }, // take the first 140 characters of the description
// metaImage() { return template.value.short_description?.substring(0, 140) + '... | Customize any template and create your own form in minutes.'
// if (!this.template) return null },
// return this.template.image_url ogImage () {
// }, if (!template || !template.value) return null
// metaTags() { return template.value.image_url
// if (!this.template) { },
// return []; robots () {
// } if (!template || !template.value) return null
// return this.template.publicly_listed ? [] : [{name: 'robots', content: 'noindex'}] return template.value.publicly_listed ? null : 'noindex'
// }, }
})
</script> </script>
<style lang='scss'> <style lang='scss'>

View File

@ -26,10 +26,10 @@ defineRouteRules({
prerender: true prerender: true
}) })
// props: { useOpnSeoMeta({
// metaTitle: { type: String, default: 'Templates' }, title: 'Form Templates',
// metaDescription: { type: String, default: 'Our collection of beautiful templates to create your own forms!' } description: 'Our collection of beautiful templates to create your own forms!'
// }, })
const templatesStore = useTemplatesStore() const templatesStore = useTemplatesStore()
loadAllTemplates(templatesStore) loadAllTemplates(templatesStore)

View File

@ -3,7 +3,7 @@
<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 type. We could not find this industry.
</p> </p>
<template v-else> <template v-else>
<section class="py-12 sm:py-16 bg-gray-50 border-b border-gray-200"> <section class="py-12 sm:py-16 bg-gray-50 border-b border-gray-200">
@ -23,7 +23,7 @@
</section> </section>
<templates-list :templates="templates" :filter-industries="false" :show-types="false"> <templates-list :templates="templates" :filter-industries="false" :show-industries="false">
<template #before-lists> <template #before-lists>
<section class="py-12 bg-white border-t border-gray-200 sm:py-16"> <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="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
@ -70,6 +70,29 @@ const breadcrumbs = computed(() => {
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.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!'
})
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')
) {
return titleChunk ? `${titleChunk} - OpnForm` : 'Form Templates - OpnForm'
}
return titleChunk ? titleChunk : 'Form Templates - OpnForm'
}
})
</script> </script>
<style lang='scss'> <style lang='scss'>

View File

@ -13,30 +13,28 @@
</div> </div>
</section> </section>
<!-- <templates-list :only-my="true" />--> <templates-list :templates="templates" :loading="loading" :show-types="false" :show-industries="false"/>
</div> </div>
</template> </template>
<script> <script setup>
import TemplatesList from '../../components/pages/templates/TemplatesList.vue' definePageMeta({
middleware: "auth"
})
export default { useOpnSeoMeta({
components: { TemplatesList }, title: 'My Templates',
middleware: 'auth', description: 'Our collection of beautiful templates to create your own forms!'
})
props: { let loading = ref(false)
metaTitle: { type: String, default: 'My Templates' }, let templates = ref([])
metaDescription: { type: String, default: 'Our collection of beautiful templates to create your own forms!' }
},
data () { onMounted(() => {
return {} loading.value = true
}, opnFetch('templates',{query: {onlymy: true}}).then((data) => {
loading.value = false
mounted () {}, templates.value = data
})
computed: {}, })
methods: {}
}
</script> </script>

View File

@ -71,6 +71,29 @@ const breadcrumbs = computed(() => {
const type = computed(() => templatesStore.types.get(route.params.slug)) const type = computed(() => templatesStore.types.get(route.params.slug))
useOpnSeoMeta({
title: () => {
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!'
})
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')
) {
return titleChunk ? `${titleChunk} - OpnForm` : 'Form Templates - OpnForm'
}
return titleChunk ? titleChunk : 'Form Templates - OpnForm'
}
})
</script> </script>
<style lang='scss'> <style lang='scss'>

View File

@ -13,11 +13,13 @@
</template> </template>
<script setup> <script setup>
// metaTitle: 'Terms & Conditions',
import {useNotionPagesStore} from "~/stores/notion_pages.js"; import {useNotionPagesStore} from "~/stores/notion_pages.js";
import {computed} from "vue"; import {computed} from "vue";
useOpnSeoMeta({
title: 'Terms & Conditions'
})
const notionPageStore = useNotionPagesStore() const notionPageStore = useNotionPagesStore()
await notionPageStore.load('246420da2834480ca04047b0c5a00929') await notionPageStore.load('246420da2834480ca04047b0c5a00929')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

2
client/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: