Decouple title from title block (#696)

* Decouple title from title block

* fix lint

* remove dry run for FormTitleMigration

* Skip form title migration for forms with hidden titles

* Refactor AI Form Generation with Dedicated Prompt Services

- Extract AI form generation logic from GenerateTemplate command into dedicated prompt service classes
- Update GenerateAiForm job to use new prompt generation services
- Improve GptCompleter with more robust error handling and token tracking
- Add error field to AiFormCompletion model for better error logging
- Simplify command signature from 'ai:make-form-template' to 'form:generate'

* Consolidate Template Metadata Generation with Unified Prompt Service

- Create GenerateTemplateMetadataPrompt to centralize template metadata generation
- Update GenerateTemplate command to use new consolidated metadata generation approach
- Enhance GptCompleter to support strict JSON schema validation
- Increase form prompt max length to support more complex form descriptions
- Refactor form generation to simplify metadata extraction and processing

* Implement Form Mode Strategy for Flexible Form Rendering

- Introduce FormModeStrategy to centralize form rendering logic
- Add support for different form modes: LIVE, PREVIEW, TEST, EDIT, PREFILL
- Refactor components to use mode-based rendering strategy
- Remove legacy boolean props like adminPreview and creating
- Enhance form component flexibility and reusability

* Refine Form Mode Strategy Display Behavior

- Update FormModeStrategy to hide hidden fields in PREVIEW mode
- Add FormMode getter in UrlFormPrefill component for mode-specific rendering
- Clarify mode-specific validation and display logic in form strategies

* Enhance Form Template Generation with Advanced Field Options

- Update GenerateTemplate command to use front_url for template URL output
- Expand GenerateFormPrompt with comprehensive field configuration options
- Add support for advanced field types: date with time, toggle switches, radio/checkbox selections
- Introduce field width configuration and HTML formatting for text elements
- Re-enable select, multi-select, and matrix field type definitions with enhanced configurations

* Remove Deprecated Template Metadata Generation Services

- Delete multiple AI prompt services related to template metadata generation
- Simplify GenerateTemplate command to use default values instead of complex metadata generation
- Remove GenerateTemplateMetadataPrompt and related classes like GenerateTemplateDescriptionPrompt, GenerateTemplateImageKeywordsPrompt, etc.
- Update form template generation to use basic fallback metadata generation approach

* Restore GenerateTemplateMetadataPrompt for Comprehensive Template Generation

- Reintroduce GenerateTemplateMetadataPrompt to replace default metadata generation
- Update GenerateTemplate command to use consolidated metadata generation approach
- Extract detailed metadata components including title, description, industries, and image search query
- Improve template generation with more dynamic and AI-generated metadata

* Refactor Template Preview Section Layout

- Remove unnecessary nested div in template preview section
- Simplify HTML structure for the template preview component
- Maintain existing styling and functionality while improving code readability

* Refactor Constructor and Code Formatting in AI Form Generation and Prompt Classes

- Updated the constructor in GenerateAiForm.php to use a block structure for improved readability and consistency.
- Added a blank line in the Prompt.php file to enhance code formatting and maintain consistency with PHP coding standards.
- Modified the migration file to use a more concise class declaration syntax, improving clarity.

These changes aim to enhance code readability and maintainability across the affected files.

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2025-03-21 21:29:18 +05:30
committed by GitHub
parent d2b8572d75
commit aa5c1acf3a
28 changed files with 1345 additions and 457 deletions

View File

@@ -7,10 +7,10 @@
<open-form
:theme="theme"
:loading="false"
:show-hidden="true"
:form="form"
:fields="form.properties"
:default-data-form="submission"
:mode="FormMode.EDIT"
@submit="updateForm"
>
<template #submit-btn="{ submitForm }">
@@ -29,6 +29,7 @@
import { ref, defineProps, defineEmits } from "vue"
import OpenForm from "../forms/OpenForm.vue"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
const props = defineProps({
show: { type: Boolean, required: true },

View File

@@ -6,24 +6,10 @@
:style="{ '--font-family': form.font_family, 'direction': form?.layout_rtl ? 'rtl' : 'ltr' }"
>
<link
v-if="adminPreview && form.font_family"
v-if="formModeStrategy.display.showFontLink && form.font_family"
rel="stylesheet"
:href="getFontUrl"
>
<template v-if="!isHideTitle">
<EditableTag
v-if="adminPreview"
v-model="form.title"
element="h1"
class="mb-2"
/>
<h1
v-else
class="mb-2 px-2"
v-text="form.title"
/>
</template>
<div v-if="isPublicFormPage && form.is_password_protected">
<p class="form-description mb-4 text-gray-700 dark:text-gray-300 px-2">
@@ -95,7 +81,7 @@
</div>
<form-cleanings
v-if="!adminPreview"
v-if="formModeStrategy.display.showFormCleanings"
:hideable="true"
class="mb-4 mx-2"
:form="form"
@@ -114,7 +100,7 @@
:fields="form.properties"
:theme="theme"
:dark-mode="darkMode"
:admin-preview="adminPreview"
:mode="mode"
@submit="submitForm"
>
<template #submit-btn="{submitForm: handleSubmit}">
@@ -208,14 +194,18 @@ import {pendingSubmission} from "~/composables/forms/pendingSubmission.js"
import clonedeep from "clone-deep"
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
import FirstSubmissionModal from '~/components/open/forms/components/FirstSubmissionModal.vue'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
components: { VTransition, OpenFormButton, OpenForm, FormCleanings, FirstSubmissionModal },
props: {
form: { type: Object, required: true },
creating: { type: Boolean, default: false }, // If true, fake form submit
adminPreview: { type: Boolean, default: false }, // If used in FormEditorPreview
mode: {
type: String,
default: FormMode.LIVE,
validator: (value) => Object.values(FormMode).includes(value)
},
submitButtonClass: { type: String, default: '' },
darkMode: {
type: Boolean,
@@ -254,6 +244,12 @@ export default {
},
computed: {
/**
* Gets the comprehensive strategy based on the form mode
*/
formModeStrategy() {
return createFormModeStrategy(this.mode)
},
isEmbedPopup () {
return import.meta.client && window.location.href.includes('popup=true')
},
@@ -266,9 +262,6 @@ export default {
isPublicFormPage () {
return this.$route.name === 'forms-slug'
},
isHideTitle () {
return this.form.hide_title || (import.meta.client && window.location.href.includes('hide_title=true'))
},
getFontUrl() {
if(!this.form || !this.form.font_family) return null
const family = this.form?.font_family.replace(/ /g, '+')
@@ -292,7 +285,8 @@ export default {
methods: {
submitForm (form, onFailure) {
if (this.creating) {
// Check if we should perform actual submission based on the mode
if (!this.formModeStrategy.validation.performActualSubmission) {
this.submitted = true
this.$emit('submitted', true)
return

View File

@@ -56,7 +56,7 @@
ghost-class="ghost-item"
filter=".not-draggable"
:animation="200"
:disabled="!adminPreview"
:disabled="!formModeStrategy.admin.allowDragging"
@change="handleDragDropped"
>
<template #item="{element}">
@@ -68,7 +68,7 @@
:data-form-value="dataFormValue"
:theme="theme"
:dark-mode="darkMode"
:admin-preview="adminPreview"
:mode="mode"
/>
</template>
</draggable>
@@ -133,6 +133,7 @@ import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import FormTimer from './FormTimer.vue'
import { storeToRefs } from 'pinia'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
name: 'OpenForm',
@@ -155,17 +156,16 @@ export default {
type: Boolean,
required: true
},
showHidden: {
type: Boolean,
default: false
},
fields: {
type: Array,
required: true
},
defaultDataForm: { type: [Object, null] },
adminPreview: {type: Boolean, default: false}, // If used in FormEditorPreview
urlPrefillPreview: {type: Boolean, default: false}, // If used in UrlFormPrefill
mode: {
type: String,
default: FormMode.LIVE,
validator: (value) => Object.values(FormMode).includes(value)
},
darkMode: {
type: Boolean,
default: false
@@ -227,6 +227,24 @@ export default {
groups.push(currentGroup)
return groups
},
/**
* Gets the comprehensive strategy based on the form mode
*/
formModeStrategy() {
return createFormModeStrategy(this.mode)
},
/**
* Determines if hidden fields should be shown based on the mode
*/
showHidden() {
return this.formModeStrategy.display.showHiddenFields
},
/**
* Determines if the form is in admin preview mode
*/
isAdminPreview() {
return this.formModeStrategy.admin.showAdminControls
},
formProgress() {
const requiredFields = this.fields.filter(field => field.required)
if (requiredFields.length === 0) {
@@ -343,14 +361,14 @@ export default {
// These watchers ensure the form shows the correct page for the field being edited in admin preview
selectedFieldIndex: {
handler(newIndex) {
if (this.adminPreview && this.showEditFieldSidebar) {
if (this.isAdminPreview && this.showEditFieldSidebar) {
this.setPageForField(newIndex)
}
}
},
showEditFieldSidebar: {
handler(newValue) {
if (this.adminPreview && newValue) {
if (this.isAdminPreview && newValue) {
this.setPageForField(this.selectedFieldIndex)
}
}
@@ -386,6 +404,12 @@ export default {
this.$refs['form-timer'].stopTimer()
this.dataForm.completion_time = this.$refs['form-timer'].completionTime
// Add validation strategy check
if (!this.formModeStrategy.validation.validateOnSubmit) {
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
return
}
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
},
/**
@@ -538,11 +562,12 @@ export default {
this.scrollToTop()
},
nextPage() {
if (this.adminPreview || this.urlPrefillPreview) {
if (!this.formModeStrategy.validation.validateOnNextPage) {
this.formPageIndex++
this.scrollToTop()
return false
}
const fieldsToValidate = this.currentFields.map(f => f.id)
this.dataForm.busy = true
this.dataForm.validate('POST', '/forms/' + this.form.slug + '/answer', {}, fieldsToValidate)

View File

@@ -7,19 +7,19 @@
:class="[
getFieldWidthClasses(field),
{
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': adminPreview,
'cursor-pointer':workingFormStore.showEditFieldSidebar && adminPreview,
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': isAdminPreview,
'cursor-pointer':workingFormStore.showEditFieldSidebar && isAdminPreview,
'bg-blue-50 hover:!bg-blue-50 dark:bg-gray-800 rounded-md': beingEdited,
}]"
@click="setFieldAsSelected"
>
<div
class="-m-[1px] w-full max-w-full mx-auto"
:class="{'relative transition-colors':adminPreview}"
:class="{'relative transition-colors':isAdminPreview}"
>
<div
v-if="adminPreview"
class="absolute translate-y-full lg:translate-y-0 -bottom-1 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block z-50"
v-if="isAdminPreview"
class="absolute translate-y-full lg:translate-y-0 -bottom-1 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block"
>
<div
class="flex lg:flex-col bg-white !bg-white dark:!bg-white border rounded-md shadow-sm z-50 p-[1px] relative"
@@ -151,6 +151,7 @@ import {computed} from 'vue'
import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import {default as _has} from 'lodash/has'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
name: 'OpenFormField',
@@ -189,16 +190,21 @@ export default {
type: Object,
required: true
},
adminPreview: {type: Boolean, default: false} // If used in FormEditorPreview
mode: {
type: String,
default: FormMode.LIVE
}
},
setup() {
setup(props) {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
currentWorkspace: computed(() => useWorkspacesStore().getCurrent),
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar)
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar),
formModeStrategy: computed(() => createFormModeStrategy(props.mode)),
isAdminPreview: computed(() => createFormModeStrategy(props.mode).admin.showAdminControls)
}
},
@@ -261,7 +267,7 @@ export default {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled()
},
beingEdited() {
return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return this.isAdminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return item.id === this.field.id
}) === this.selectedFieldIndex
},
@@ -281,7 +287,7 @@ export default {
return fieldsOptions
},
fieldSideBarOpened() {
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
return this.isAdminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
}
},
@@ -292,19 +298,19 @@ export default {
methods: {
editFieldOptions() {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.openSettingsForField(this.field)
},
setFieldAsSelected () {
if (!this.adminPreview || !this.workingFormStore.showEditFieldSidebar) return
if (!this.formModeStrategy.admin.showAdminControls || !this.workingFormStore.showEditFieldSidebar) return
this.workingFormStore.openSettingsForField(this.field)
},
openAddFieldSidebar() {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.openAddFieldSidebar(this.field)
},
removeField () {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.removeField(this.field)
},
getFieldWidthClasses(field) {

View File

@@ -24,15 +24,6 @@
</h3>
</div>
</template>
<toggle-switch-input
:model-value="modelValue.hide_title"
name="hide_title"
class="mt-4"
label="Hide Form Title"
:disabled="form.hide_title === true ? true : null"
:help="hideTitleHelp"
@update:model-value="onChangeHideTitle"
/>
<toggle-switch-input
:model-value="modelValue.auto_submit"
name="auto_submit"
@@ -64,22 +55,7 @@ export default {
return {}
},
computed: {
hideTitleHelp() {
return this.form.hide_title
? "This option is disabled because the form title is already hidden"
: null
},
},
watch: {},
mounted() {},
methods: {
onChangeHideTitle(val) {
this.modelValue.hide_title = val
},
onChangeAutoSubmit(val) {
this.modelValue.auto_submit = val
},

View File

@@ -148,11 +148,6 @@
label="Color image"
help="Not visible when form is embedded"
/>
<toggle-switch-input
name="hide_title"
:form="form"
label="Hide Title"
/>
<toggle-switch-input
name="no_branding"
:form="form"
@@ -173,11 +168,6 @@
icon="heroicons:cog-6-tooth-16-solid"
title="Advanced Options"
/>
<toggle-switch-input
name="hide_title"
:form="form"
label="Hide Form Title"
/>
<toggle-switch-input
name="show_progress_bar"
:form="form"

View File

@@ -92,11 +92,9 @@
ref="formPreview"
class="w-full mx-auto py-5"
:class="{'max-w-lg': form && (form.width === 'centered'),'px-7': !isExpanded, 'px-3': isExpanded}"
:creating="creating"
:form="form"
:dark-mode="darkMode"
:admin-preview="!isExpanded"
:show-cleanings="false"
:mode="formMode"
@restarted="previewFormSubmitted=false"
@submitted="previewFormSubmitted=true"
/>
@@ -113,6 +111,7 @@ import { default as _has } from 'lodash/has'
import { useRecordsStore } from '~/stores/records'
import { useWorkingFormStore } from '~/stores/working_form'
import { storeToRefs } from 'pinia'
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
const recordsStore = useRecordsStore()
const workingFormStore = useWorkingFormStore()
@@ -126,7 +125,8 @@ const { content: form } = storeToRefs(workingFormStore)
const recordLoading = computed(() => recordsStore.loading)
const darkMode = useDarkMode(parent)
const creating = computed(() => !_has(form.value, 'id'))
// Use PREVIEW mode when not expanded, TEST mode when expanded
const formMode = computed(() => isExpanded.value ? FormMode.TEST : FormMode.PREVIEW)
defineShortcuts({
escape: {

View File

@@ -108,14 +108,6 @@
</div>
</template>
<div class="border-t mt-4 -mx-4" />
<toggle-switch-input
v-model="advancedOptions.hide_title"
name="hide_title"
class="mt-4"
label="Hide Form Title"
:disabled="form.hide_title === true ? true : null"
:help="hideTitleHelp"
/>
<color-input
v-model="advancedOptions.bgcolor"
name="bgcolor"
@@ -175,22 +167,14 @@ const props = defineProps({
const embedScriptUrl = "/widgets/embed-min.js"
const showEmbedFormAsPopupModal = ref(false)
const advancedOptions = ref({
hide_title: false,
emoji: "💬",
position: "right",
bgcolor: "#3B82F6",
width: "500",
})
const hideTitleHelp = computed(() => {
return props.form.hide_title
? "This option is disabled because the form title is already hidden"
: null
})
const shareUrl = computed(() => {
return advancedOptions.value.hide_title
? props.form.share_url + "?hide_title=true"
: props.form.share_url
return props.form.share_url
})
const embedPopupCode = computed(() => {
const nfData = {

View File

@@ -75,10 +75,9 @@
v-if="form"
:theme="theme"
:loading="false"
:show-hidden="true"
:form="form"
:fields="form.properties"
:url-prefill-preview="true"
:mode="FormMode.PREFILL"
@submit="generateUrl"
>
<template #submit-btn="{ submitForm }">
@@ -111,6 +110,7 @@
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder"
import FormUrlPrefill from "../../../open/forms/components/FormUrlPrefill.vue"
import OpenForm from "../../../open/forms/OpenForm.vue"
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
export default {
name: "UrlFormPrefill",
@@ -132,6 +132,9 @@ export default {
borderRadius: this.form.border_radius
}).getAllComponents()
},
FormMode() {
return FormMode
}
},
methods: {

View File

@@ -17,7 +17,6 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
layout_rtl: false,
dark_mode: "auto",
color: DEFAULT_COLOR,
hide_title: false,
no_branding: false,
uppercase_labels: false,
transparent_background: false,
@@ -55,6 +54,12 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
function getDefaultProperties() {
return [
{
type: "nf-text",
content: "<h1>My Form</h1>",
name: "Title",
id: generateUUID(),
},
{
name: "Name",
type: "text",
@@ -96,7 +101,6 @@ export function setFormDefaults(formData) {
border_radius: 'small',
dark_mode: 'light',
color: '#3B82F6',
hide_title: false,
uppercase_labels: false,
no_branding: false,
transparent_background: false,

92
client/lib/forms/FormModeStrategy.js vendored Normal file
View File

@@ -0,0 +1,92 @@
/**
* Form modes for OpenForm components
*/
export const FormMode = {
LIVE: 'live', // Real form with full validation and submission
PREVIEW: 'preview', // Admin preview with no validation
PREFILL: 'prefill', // URL prefill preview with no validation
EDIT: 'edit', // Editing an existing submission
TEST: 'test' // Test mode with validation but no actual submission
}
/**
* Creates a comprehensive strategy based on the form mode
* This handles all mode-specific behaviors, not just validation
*
* @param {string} mode - One of the FormMode values
* @returns {Object} - Strategy object with all mode-specific behaviors
*/
export function createFormModeStrategy(mode) {
// Default configuration (LIVE mode)
const defaultStrategy = {
// Validation behaviors
validation: {
validateOnNextPage: true,
validateOnSubmit: true,
performActualSubmission: true
},
// Display behaviors
display: {
showHiddenFields: false,
showFormCleanings: true,
showFontLink: false
},
// Admin behaviors
admin: {
allowDragging: false,
showAdminControls: false,
isEditingMode: false
}
}
// Return default strategy for LIVE mode or unknown modes
if (mode === FormMode.LIVE || !Object.values(FormMode).includes(mode)) {
return defaultStrategy
}
// Create a copy of the default strategy to modify
const strategy = JSON.parse(JSON.stringify(defaultStrategy))
// Apply mode-specific overrides
switch (mode) {
case FormMode.PREVIEW:
// Admin preview - no validation, show admin controls but NOT hidden fields
strategy.validation.validateOnNextPage = false
strategy.validation.validateOnSubmit = false
strategy.validation.performActualSubmission = false
strategy.display.showHiddenFields = false
strategy.display.showFormCleanings = false
strategy.display.showFontLink = true
strategy.admin.allowDragging = true
strategy.admin.showAdminControls = true
break
case FormMode.PREFILL:
// URL prefill - no validation, show hidden fields
strategy.validation.validateOnNextPage = false
strategy.validation.validateOnSubmit = false
strategy.validation.performActualSubmission = false
strategy.display.showHiddenFields = true
break
case FormMode.EDIT:
// Editing submission - same validation as LIVE mode, but show hidden fields
// This ensures edit mode behaves like live mode for validation
strategy.display.showHiddenFields = true
strategy.admin.isEditingMode = true
break
case FormMode.TEST:
// Test mode - validate on submit but don't submit, and don't validate on next page
strategy.validation.performActualSubmission = false
strategy.validation.validateOnNextPage = false
break
}
return strategy
}

View File

@@ -54,6 +54,7 @@
:form="form"
class="mb-10"
:dark-mode="darkMode"
:mode="FormMode.LIVE"
@password-entered="passwordEntered"
/>
</template>
@@ -72,6 +73,7 @@ import {
focusOnFirstFormElement,
useDarkMode
} from '~/lib/forms/public-page'
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
const crisp = useCrisp()
const formsStore = useFormsStore()

View File

@@ -69,7 +69,6 @@ useOpnSeoMeta({
})
const shareFormConfig = ref({
hide_title: false,
auto_submit: false,
})

View File

@@ -57,17 +57,15 @@
<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"
class="flex flex-col items-center justify-center max-w-5xl 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"
class="aspect-[4/3] shrink-0 rounded-lg shadow-sm overflow-hidden group w-full max-w-sm relative"
>
<img
class="object-cover w-full transition-all duration-200 group-hover:scale-110 h-[240px]"
class="object-cover w-full h-full transition-all duration-200 group-hover:scale-110 absolute inset-0"
:src="template.image_url"
alt="Template cover image"
width="500px"
height="380px"
>
</div>
@@ -90,21 +88,19 @@
</div>
</section>
<section class="relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
<div class="max-w-7xl">
<div
class="max-w-2xl p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
>
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
Template Preview
</p>
<open-complete-form
ref="open-complete-form"
:form="form"
:creating="true"
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
/>
</div>
<section class="w-full max-w-4xl relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
<div
class="p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
>
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
Template Preview
</p>
<open-complete-form
ref="open-complete-form"
:form="form"
:mode="FormMode.TEST"
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
/>
</div>
<div class="absolute bottom-0 translate-y-full inset-x-0">
@@ -132,7 +128,7 @@
<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"
class="max-w-4xl mx-auto mt-16 space-y-12 sm:mt-16 sm:space-y-16"
>
<div
class="nf-text"
@@ -216,7 +212,7 @@
</h4>
</div>
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12">
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12 max-w-5xl mx-auto">
<div
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
>
@@ -276,6 +272,7 @@ 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 { FormMode } from "~/lib/forms/FormModeStrategy.js"
defineRouteRules({
swr: 3600,