@edgedev/create-edge-app 1.1.27 → 1.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/edge/components/auth/register.vue +51 -0
- package/edge/components/cms/block.vue +363 -42
- package/edge/components/cms/blockEditor.vue +50 -3
- package/edge/components/cms/codeEditor.vue +39 -2
- package/edge/components/cms/htmlContent.vue +10 -2
- package/edge/components/cms/init_blocks/footer.html +111 -19
- package/edge/components/cms/init_blocks/image.html +8 -0
- package/edge/components/cms/init_blocks/post_content.html +3 -2
- package/edge/components/cms/init_blocks/post_title_header.html +8 -6
- package/edge/components/cms/init_blocks/posts_list.html +6 -5
- package/edge/components/cms/mediaCard.vue +13 -2
- package/edge/components/cms/mediaManager.vue +35 -5
- package/edge/components/cms/menu.vue +384 -61
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +160 -18
- package/edge/components/cms/site.vue +548 -374
- package/edge/components/cms/siteSettingsForm.vue +623 -0
- package/edge/components/cms/themeDefaultMenu.vue +258 -22
- package/edge/components/cms/themeEditor.vue +95 -11
- package/edge/components/editor.vue +1 -0
- package/edge/components/formSubtypes/myOrgs.vue +112 -1
- package/edge/components/imagePicker.vue +126 -0
- package/edge/components/myAccount.vue +1 -0
- package/edge/components/myProfile.vue +345 -61
- package/edge/components/orgSwitcher.vue +1 -1
- package/edge/components/organizationMembers.vue +620 -235
- package/edge/components/shad/html.vue +6 -0
- package/edge/components/shad/number.vue +2 -2
- package/edge/components/sideBar.vue +7 -4
- package/edge/components/sideBarContent.vue +1 -1
- package/edge/components/userMenu.vue +50 -14
- package/edge/composables/global.ts +4 -1
- package/edge/composables/siteSettingsTemplate.js +79 -0
- package/edge/composables/structuredDataTemplates.js +36 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="js">
|
|
2
2
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
3
3
|
import * as z from 'zod'
|
|
4
|
+
import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Inbox, Loader2, Mail, MailOpen, MoreHorizontal } from 'lucide-vue-next'
|
|
5
|
+
import { useStructuredDataTemplates } from '@/edge/composables/structuredDataTemplates'
|
|
4
6
|
|
|
5
|
-
import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Loader2, MoreHorizontal } from 'lucide-vue-next'
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
site: {
|
|
8
9
|
type: String,
|
|
@@ -15,6 +16,8 @@ const props = defineProps({
|
|
|
15
16
|
},
|
|
16
17
|
})
|
|
17
18
|
const edgeFirebase = inject('edgeFirebase')
|
|
19
|
+
const { createDefaults: createSiteSettingsDefaults, createNewDocSchema: createSiteSettingsNewDocSchema } = useSiteSettingsTemplate()
|
|
20
|
+
const { buildPageStructuredData } = useStructuredDataTemplates()
|
|
18
21
|
|
|
19
22
|
const normalizeForCompare = (value) => {
|
|
20
23
|
if (Array.isArray(value))
|
|
@@ -30,35 +33,35 @@ const normalizeForCompare = (value) => {
|
|
|
30
33
|
|
|
31
34
|
const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
|
|
32
35
|
const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
|
|
36
|
+
const isJsonInvalid = (value) => {
|
|
37
|
+
if (value === null || value === undefined)
|
|
38
|
+
return false
|
|
39
|
+
if (typeof value === 'object')
|
|
40
|
+
return false
|
|
41
|
+
const text = String(value).trim()
|
|
42
|
+
if (!text)
|
|
43
|
+
return false
|
|
44
|
+
try {
|
|
45
|
+
JSON.parse(text)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
|
|
34
53
|
const isTemplateSite = computed(() => props.site === 'templates')
|
|
35
54
|
const router = useRouter()
|
|
36
55
|
|
|
56
|
+
const SUBMISSION_IGNORE_FIELDS = new Set(['orgId', 'siteId', 'pageId', 'blockId'])
|
|
57
|
+
const SUBMISSION_LABEL_KEYS = ['name', 'fullName', 'firstName', 'lastName', 'email', 'phone']
|
|
58
|
+
const SUBMISSION_MESSAGE_KEYS = ['message', 'comments', 'notes', 'inquiry', 'details']
|
|
59
|
+
|
|
37
60
|
const state = reactive({
|
|
38
61
|
filter: '',
|
|
39
62
|
userFilter: 'all',
|
|
40
63
|
newDocs: {
|
|
41
|
-
sites:
|
|
42
|
-
name: { bindings: { 'field-type': 'text', 'label': 'Name' }, cols: '12', value: '' },
|
|
43
|
-
theme: { bindings: { 'field-type': 'collection', 'label': 'Themes', 'collection-path': 'themes' }, cols: '12', value: '' },
|
|
44
|
-
allowedThemes: { bindings: { 'field-type': 'tags', 'label': 'Allowed Themes' }, cols: '12', value: [] },
|
|
45
|
-
logo: { bindings: { 'field-type': 'text', 'label': 'Dark logo' }, cols: '12', value: '' },
|
|
46
|
-
logoLight: { bindings: { 'field-type': 'text', 'label': 'Logo Light' }, cols: '12', value: '' },
|
|
47
|
-
logoText: { bindings: { 'field-type': 'text', 'label': 'Logo Text' }, cols: '12', value: '' },
|
|
48
|
-
logoType: { bindings: { 'field-type': 'select', 'label': 'Logo Type', 'items': ['image', 'text'] }, cols: '12', value: 'image' },
|
|
49
|
-
brandLogoDark: { bindings: { 'field-type': 'text', 'label': 'Brand Logo Dark' }, cols: '12', value: '' },
|
|
50
|
-
brandLogoLight: { bindings: { 'field-type': 'text', 'label': 'Brand Logo Light' }, cols: '12', value: '' },
|
|
51
|
-
favicon: { bindings: { 'field-type': 'text', 'label': 'Favicon' }, cols: '12', value: '' },
|
|
52
|
-
menuPosition: { bindings: { 'field-type': 'select', 'label': 'Menu Position', 'items': ['left', 'center', 'right'] }, cols: '12', value: 'right' },
|
|
53
|
-
domains: { bindings: { 'field-type': 'tags', 'label': 'Domains', 'helper': 'Add or remove domains' }, cols: '12', value: [] },
|
|
54
|
-
contactEmail: { bindings: { 'field-type': 'text', 'label': 'Contact Email' }, cols: '12', value: '' },
|
|
55
|
-
metaTitle: { bindings: { 'field-type': 'text', 'label': 'Meta Title' }, cols: '12', value: '' },
|
|
56
|
-
metaDescription: { bindings: { 'field-type': 'textarea', 'label': 'Meta Description' }, cols: '12', value: '' },
|
|
57
|
-
structuredData: { bindings: { 'field-type': 'textarea', 'label': 'Structured Data (JSON-LD)' }, cols: '12', value: '' },
|
|
58
|
-
users: { bindings: { 'field-type': 'users', 'label': 'Users', 'hint': 'Choose users' }, cols: '12', value: [] },
|
|
59
|
-
aiAgentUserId: { bindings: { 'field-type': 'select', 'label': 'Agent Data for AI to use to build initial site' }, cols: '12', value: '' },
|
|
60
|
-
aiInstructions: { bindings: { 'field-type': 'textarea', 'label': 'Additional AI Instructions' }, cols: '12', value: '' },
|
|
61
|
-
},
|
|
64
|
+
sites: createSiteSettingsNewDocSchema(),
|
|
62
65
|
},
|
|
63
66
|
mounted: false,
|
|
64
67
|
page: {},
|
|
@@ -67,20 +70,21 @@ const state = reactive({
|
|
|
67
70
|
siteSettings: false,
|
|
68
71
|
hasError: false,
|
|
69
72
|
updating: false,
|
|
70
|
-
logoPickerOpen: false,
|
|
71
|
-
logoLightPickerOpen: false,
|
|
72
|
-
brandLogoDarkPickerOpen: false,
|
|
73
|
-
brandLogoLightPickerOpen: false,
|
|
74
|
-
faviconPickerOpen: false,
|
|
75
73
|
aiSectionOpen: false,
|
|
76
74
|
selectedPostId: '',
|
|
77
75
|
viewMode: 'pages',
|
|
76
|
+
submissionFilter: '',
|
|
77
|
+
selectedSubmissionId: '',
|
|
78
|
+
publishSiteLoading: false,
|
|
78
79
|
})
|
|
79
80
|
|
|
80
81
|
const pageInit = {
|
|
81
82
|
name: '',
|
|
82
83
|
content: [],
|
|
83
84
|
blockIds: [],
|
|
85
|
+
metaTitle: '',
|
|
86
|
+
metaDescription: '',
|
|
87
|
+
structuredData: buildPageStructuredData(),
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
const schemas = {
|
|
@@ -95,6 +99,7 @@ const schemas = {
|
|
|
95
99
|
path: ['domains', 0],
|
|
96
100
|
}),
|
|
97
101
|
contactEmail: z.string().optional(),
|
|
102
|
+
contactPhone: z.string().optional(),
|
|
98
103
|
theme: z.string({
|
|
99
104
|
required_error: 'Theme is required',
|
|
100
105
|
}).min(1, { message: 'Theme is required' }),
|
|
@@ -110,6 +115,15 @@ const schemas = {
|
|
|
110
115
|
metaTitle: z.string().optional(),
|
|
111
116
|
metaDescription: z.string().optional(),
|
|
112
117
|
structuredData: z.string().optional(),
|
|
118
|
+
trackingFacebookPixel: z.string().optional(),
|
|
119
|
+
trackingGoogleAnalytics: z.string().optional(),
|
|
120
|
+
trackingAdroll: z.string().optional(),
|
|
121
|
+
socialFacebook: z.string().optional(),
|
|
122
|
+
socialInstagram: z.string().optional(),
|
|
123
|
+
socialTwitter: z.string().optional(),
|
|
124
|
+
socialLinkedIn: z.string().optional(),
|
|
125
|
+
socialYouTube: z.string().optional(),
|
|
126
|
+
socialTikTok: z.string().optional(),
|
|
113
127
|
aiAgentUserId: z.string().optional(),
|
|
114
128
|
aiInstructions: z.string().optional(),
|
|
115
129
|
})),
|
|
@@ -127,6 +141,185 @@ const isAdmin = computed(() => {
|
|
|
127
141
|
const siteData = computed(() => {
|
|
128
142
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
|
|
129
143
|
})
|
|
144
|
+
const publishedSiteSettings = computed(() => {
|
|
145
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site] || {}
|
|
146
|
+
})
|
|
147
|
+
const domainError = computed(() => {
|
|
148
|
+
return String(publishedSiteSettings.value?.domainError || '').trim()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const submissionsCollection = computed(() => `sites/${props.site}/lead-actions`)
|
|
152
|
+
const isViewingSubmissions = computed(() => state.viewMode === 'submissions')
|
|
153
|
+
const submissionsMap = computed(() => {
|
|
154
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`] || {}
|
|
155
|
+
})
|
|
156
|
+
const selectedSubmission = computed(() => {
|
|
157
|
+
return submissionsMap.value?.[state.selectedSubmissionId] || null
|
|
158
|
+
})
|
|
159
|
+
const unreadSubmissionsCount = computed(() => {
|
|
160
|
+
return Object.values(submissionsMap.value || {}).filter((item) => {
|
|
161
|
+
if (item?.action !== 'Contact Form')
|
|
162
|
+
return false
|
|
163
|
+
return !item.readAt
|
|
164
|
+
}).length
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const formatSubmissionValue = (value) => {
|
|
168
|
+
if (value === undefined || value === null)
|
|
169
|
+
return ''
|
|
170
|
+
if (typeof value === 'string')
|
|
171
|
+
return value
|
|
172
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
173
|
+
return String(value)
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(value)
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return String(value)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const collectSubmissionEntries = (data) => {
|
|
183
|
+
if (!data || typeof data !== 'object')
|
|
184
|
+
return []
|
|
185
|
+
const entries = []
|
|
186
|
+
const seen = new Set()
|
|
187
|
+
const addEntry = (key, value) => {
|
|
188
|
+
const normalizedKey = String(key || '').trim()
|
|
189
|
+
if (!normalizedKey)
|
|
190
|
+
return
|
|
191
|
+
const lowerKey = normalizedKey.toLowerCase()
|
|
192
|
+
if (SUBMISSION_IGNORE_FIELDS.has(normalizedKey) || SUBMISSION_IGNORE_FIELDS.has(lowerKey))
|
|
193
|
+
return
|
|
194
|
+
if (value === undefined || value === null || value === '')
|
|
195
|
+
return
|
|
196
|
+
if (seen.has(lowerKey))
|
|
197
|
+
return
|
|
198
|
+
entries.push({ key: normalizedKey, value })
|
|
199
|
+
seen.add(lowerKey)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const addArrayFields = (fields) => {
|
|
203
|
+
if (!Array.isArray(fields))
|
|
204
|
+
return
|
|
205
|
+
fields.forEach((field) => {
|
|
206
|
+
if (!field)
|
|
207
|
+
return
|
|
208
|
+
const name = field.field || field.name || field.fieldName || field.label || field.title
|
|
209
|
+
const value = field.value ?? field.fieldValue ?? field.val
|
|
210
|
+
addEntry(name, value)
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
addArrayFields(data.fields)
|
|
215
|
+
addArrayFields(data.formFields)
|
|
216
|
+
addArrayFields(data.formData)
|
|
217
|
+
|
|
218
|
+
if (data.fields && typeof data.fields === 'object' && !Array.isArray(data.fields)) {
|
|
219
|
+
Object.entries(data.fields).forEach(([key, value]) => addEntry(key, value))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
223
|
+
if (key === 'fields' || key === 'formFields' || key === 'formData')
|
|
224
|
+
return
|
|
225
|
+
addEntry(key, value)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return entries.sort((a, b) => String(a.key).localeCompare(String(b.key)))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const getSubmissionLabel = (data) => {
|
|
232
|
+
if (!data || typeof data !== 'object')
|
|
233
|
+
return 'Contact Form Submission'
|
|
234
|
+
const name = [data.firstName, data.lastName].filter(Boolean).join(' ').trim()
|
|
235
|
+
if (name)
|
|
236
|
+
return name
|
|
237
|
+
const direct = SUBMISSION_LABEL_KEYS.find(key => String(data[key] || '').trim().length)
|
|
238
|
+
if (direct)
|
|
239
|
+
return String(data[direct]).trim()
|
|
240
|
+
return 'Contact Form Submission'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const getSubmissionMessage = (data) => {
|
|
244
|
+
if (!data || typeof data !== 'object')
|
|
245
|
+
return ''
|
|
246
|
+
const direct = SUBMISSION_MESSAGE_KEYS.find(key => String(data[key] || '').trim().length)
|
|
247
|
+
if (direct)
|
|
248
|
+
return String(data[direct]).trim()
|
|
249
|
+
return ''
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const formatSubmissionKey = (key) => {
|
|
253
|
+
return String(key || '')
|
|
254
|
+
.trim()
|
|
255
|
+
.replace(/_/g, ' ')
|
|
256
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
257
|
+
.replace(/\s+/g, ' ')
|
|
258
|
+
.replace(/^./, str => str.toUpperCase())
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const getSubmissionEntriesPreview = (data, limit = 6) => {
|
|
262
|
+
return collectSubmissionEntries(data).slice(0, limit)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const formatSubmissionTimestamp = (timestamp) => {
|
|
266
|
+
const date = timestamp?.toDate?.() || (timestamp ? new Date(timestamp) : null)
|
|
267
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
268
|
+
return ''
|
|
269
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
270
|
+
dateStyle: 'medium',
|
|
271
|
+
timeStyle: 'short',
|
|
272
|
+
}).format(date)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const isSubmissionUnread = item => item && item.action === 'Contact Form' && !item.readAt
|
|
276
|
+
|
|
277
|
+
const markSubmissionRead = async (docId) => {
|
|
278
|
+
const item = submissionsMap.value?.[docId]
|
|
279
|
+
if (!item || !isSubmissionUnread(item))
|
|
280
|
+
return
|
|
281
|
+
try {
|
|
282
|
+
await edgeFirebase.changeDoc(
|
|
283
|
+
`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
|
|
284
|
+
docId,
|
|
285
|
+
{ readAt: new Date().toISOString() },
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error('Failed to mark submission as read', error)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const markSubmissionUnread = async (docId) => {
|
|
294
|
+
const item = submissionsMap.value?.[docId]
|
|
295
|
+
if (!item || isSubmissionUnread(item))
|
|
296
|
+
return
|
|
297
|
+
try {
|
|
298
|
+
await edgeFirebase.changeDoc(
|
|
299
|
+
`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
|
|
300
|
+
docId,
|
|
301
|
+
{ readAt: null },
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
console.error('Failed to mark submission as unread', error)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const getSubmissionSortTime = (item) => {
|
|
310
|
+
const date = item?.timestamp?.toDate?.() || (item?.timestamp ? new Date(item.timestamp) : null)
|
|
311
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
312
|
+
return 0
|
|
313
|
+
return date.getTime()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const sortedSubmissionIds = computed(() => {
|
|
317
|
+
return Object.values(submissionsMap.value || {})
|
|
318
|
+
.filter(item => item?.docId)
|
|
319
|
+
.map(item => ({ id: item.docId, time: getSubmissionSortTime(item) }))
|
|
320
|
+
.sort((a, b) => b.time - a.time)
|
|
321
|
+
.map(item => item.id)
|
|
322
|
+
})
|
|
130
323
|
|
|
131
324
|
const themeCollection = computed(() => {
|
|
132
325
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {}
|
|
@@ -162,8 +355,9 @@ const themeOptionsMap = computed(() => {
|
|
|
162
355
|
const orgUsers = computed(() => edgeFirebase.state?.users || {})
|
|
163
356
|
const userOptions = computed(() => {
|
|
164
357
|
return Object.entries(orgUsers.value || {})
|
|
358
|
+
.filter(([, user]) => Boolean(user?.userId))
|
|
165
359
|
.map(([id, user]) => ({
|
|
166
|
-
value: user?.userId
|
|
360
|
+
value: user?.userId,
|
|
167
361
|
label: user?.meta?.name || user?.userId || id,
|
|
168
362
|
}))
|
|
169
363
|
.sort((a, b) => a.label.localeCompare(b.label))
|
|
@@ -197,6 +391,8 @@ const menuPositionOptions = [
|
|
|
197
391
|
{ value: 'right', label: 'Right' },
|
|
198
392
|
]
|
|
199
393
|
|
|
394
|
+
const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
|
|
395
|
+
|
|
200
396
|
const TEMPLATE_PAGES_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
|
|
201
397
|
const seededSiteIds = new Set()
|
|
202
398
|
|
|
@@ -279,6 +475,7 @@ const deriveBlockIdsFromDoc = (doc = {}) => {
|
|
|
279
475
|
|
|
280
476
|
const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') => {
|
|
281
477
|
const timestamp = Date.now()
|
|
478
|
+
const templateStructuredData = typeof templateDoc?.structuredData === 'string' ? templateDoc.structuredData.trim() : ''
|
|
282
479
|
const payload = {
|
|
283
480
|
name: displayName?.trim()?.length ? displayName : titleFromSlug(slug),
|
|
284
481
|
slug,
|
|
@@ -290,7 +487,7 @@ const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') =>
|
|
|
290
487
|
blockIds: [],
|
|
291
488
|
metaTitle: templateDoc?.metaTitle || '',
|
|
292
489
|
metaDescription: templateDoc?.metaDescription || '',
|
|
293
|
-
structuredData:
|
|
490
|
+
structuredData: templateStructuredData || buildPageStructuredData(),
|
|
294
491
|
doc_created_at: timestamp,
|
|
295
492
|
last_updated: timestamp,
|
|
296
493
|
}
|
|
@@ -310,6 +507,8 @@ const buildMenusFromDefaultPages = (defaultPages = []) => {
|
|
|
310
507
|
menus['Site Root'].push({
|
|
311
508
|
name: slug,
|
|
312
509
|
item: entry.pageId,
|
|
510
|
+
disableRename: !!entry?.disableRename,
|
|
511
|
+
disableDelete: !!entry?.disableDelete,
|
|
313
512
|
})
|
|
314
513
|
}
|
|
315
514
|
return menus
|
|
@@ -323,6 +522,37 @@ const deriveThemeMenus = (themeDoc = {}) => {
|
|
|
323
522
|
return null
|
|
324
523
|
}
|
|
325
524
|
|
|
525
|
+
const shouldApplyThemeSetting = (currentValue, baseValue) => {
|
|
526
|
+
if (currentValue === undefined || currentValue === null)
|
|
527
|
+
return true
|
|
528
|
+
if (typeof currentValue === 'string')
|
|
529
|
+
return !currentValue.trim() || areEqualNormalized(currentValue, baseValue)
|
|
530
|
+
if (Array.isArray(currentValue))
|
|
531
|
+
return currentValue.length === 0 || areEqualNormalized(currentValue, baseValue)
|
|
532
|
+
if (typeof currentValue === 'object')
|
|
533
|
+
return Object.keys(currentValue).length === 0 || areEqualNormalized(currentValue, baseValue)
|
|
534
|
+
return areEqualNormalized(currentValue, baseValue)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const buildThemeSettingsPayload = (themeDoc = {}, siteDoc = {}) => {
|
|
538
|
+
if (!themeDoc?.defaultSiteSettings || typeof themeDoc.defaultSiteSettings !== 'object' || Array.isArray(themeDoc.defaultSiteSettings))
|
|
539
|
+
return {}
|
|
540
|
+
const baseDefaults = createSiteSettingsDefaults()
|
|
541
|
+
const payload = {}
|
|
542
|
+
for (const [key, baseValue] of Object.entries(baseDefaults)) {
|
|
543
|
+
if (!(key in themeDoc.defaultSiteSettings))
|
|
544
|
+
continue
|
|
545
|
+
let themeValue = themeDoc.defaultSiteSettings[key]
|
|
546
|
+
if (key === 'structuredData' && typeof themeValue === 'string' && !themeValue.trim())
|
|
547
|
+
themeValue = baseValue
|
|
548
|
+
if (areEqualNormalized(themeValue, baseValue))
|
|
549
|
+
continue
|
|
550
|
+
if (shouldApplyThemeSetting(siteDoc?.[key], baseValue))
|
|
551
|
+
payload[key] = themeValue
|
|
552
|
+
}
|
|
553
|
+
return payload
|
|
554
|
+
}
|
|
555
|
+
|
|
326
556
|
const ensureTemplatePagesSnapshot = async () => {
|
|
327
557
|
if (!edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value])
|
|
328
558
|
await edgeFirebase.startSnapshot(TEMPLATE_PAGES_PATH.value)
|
|
@@ -339,6 +569,10 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
|
|
|
339
569
|
for (const entry of entries) {
|
|
340
570
|
if (!entry || entry.item == null)
|
|
341
571
|
continue
|
|
572
|
+
if (isExternalLinkEntry(entry)) {
|
|
573
|
+
next.push(edgeGlobal.dupObject(entry))
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
342
576
|
if (typeof entry.item === 'string' || entry.item === '') {
|
|
343
577
|
const templateDoc = templatePages?.[entry.item] || null
|
|
344
578
|
const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
|
|
@@ -376,21 +610,26 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
|
|
|
376
610
|
return next
|
|
377
611
|
}
|
|
378
612
|
|
|
379
|
-
const seedNewSiteFromTheme = async (siteId, themeId) => {
|
|
613
|
+
const seedNewSiteFromTheme = async (siteId, themeId, siteDoc) => {
|
|
380
614
|
if (!siteId || !themeId)
|
|
381
615
|
return
|
|
382
616
|
const themeDoc = themeCollection.value?.[themeId]
|
|
383
617
|
if (!themeDoc)
|
|
384
618
|
return
|
|
619
|
+
const updatePayload = {}
|
|
385
620
|
const themeMenus = deriveThemeMenus(themeDoc)
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
621
|
+
if (themeMenus) {
|
|
622
|
+
const templatePages = await ensureTemplatePagesSnapshot()
|
|
623
|
+
const usedSlugs = new Set()
|
|
624
|
+
const seededMenus = ensureMenuBuckets(themeMenus)
|
|
625
|
+
seededMenus['Site Root'] = await duplicateEntriesWithPages(seededMenus['Site Root'], { templatePages, siteId, usedSlugs })
|
|
626
|
+
seededMenus['Not In Menu'] = await duplicateEntriesWithPages(seededMenus['Not In Menu'], { templatePages, siteId, usedSlugs })
|
|
627
|
+
updatePayload.menus = seededMenus
|
|
628
|
+
}
|
|
629
|
+
const settingsPayload = buildThemeSettingsPayload(themeDoc, siteDoc || {})
|
|
630
|
+
Object.assign(updatePayload, settingsPayload)
|
|
631
|
+
if (Object.keys(updatePayload).length)
|
|
632
|
+
await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, siteId, updatePayload)
|
|
394
633
|
}
|
|
395
634
|
|
|
396
635
|
const handleNewSiteSaved = async ({ docId, data, collection }) => {
|
|
@@ -405,7 +644,7 @@ const handleNewSiteSaved = async ({ docId, data, collection }) => {
|
|
|
405
644
|
return
|
|
406
645
|
seededSiteIds.add(docId)
|
|
407
646
|
try {
|
|
408
|
-
await seedNewSiteFromTheme(docId, themeId)
|
|
647
|
+
await seedNewSiteFromTheme(docId, themeId, data)
|
|
409
648
|
}
|
|
410
649
|
catch (error) {
|
|
411
650
|
console.error('Failed to seed site from theme defaults', error)
|
|
@@ -438,6 +677,12 @@ onBeforeMount(async () => {
|
|
|
438
677
|
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`]) {
|
|
439
678
|
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`)
|
|
440
679
|
}
|
|
680
|
+
if (props.site !== 'templates') {
|
|
681
|
+
const submissionsPath = `organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/lead-actions`
|
|
682
|
+
if (!edgeFirebase.data?.[submissionsPath]) {
|
|
683
|
+
await edgeFirebase.startSnapshot(submissionsPath, [{ field: 'action', operator: '==', value: 'Contact Form' }])
|
|
684
|
+
}
|
|
685
|
+
}
|
|
441
686
|
state.mounted = true
|
|
442
687
|
})
|
|
443
688
|
|
|
@@ -464,9 +709,19 @@ const isSiteDiff = computed(() => {
|
|
|
464
709
|
favicon: publishedSite.favicon,
|
|
465
710
|
menuPosition: publishedSite.menuPosition,
|
|
466
711
|
contactEmail: publishedSite.contactEmail,
|
|
712
|
+
contactPhone: publishedSite.contactPhone,
|
|
467
713
|
metaTitle: publishedSite.metaTitle,
|
|
468
714
|
metaDescription: publishedSite.metaDescription,
|
|
469
715
|
structuredData: publishedSite.structuredData,
|
|
716
|
+
trackingFacebookPixel: publishedSite.trackingFacebookPixel,
|
|
717
|
+
trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics,
|
|
718
|
+
trackingAdroll: publishedSite.trackingAdroll,
|
|
719
|
+
socialFacebook: publishedSite.socialFacebook,
|
|
720
|
+
socialInstagram: publishedSite.socialInstagram,
|
|
721
|
+
socialTwitter: publishedSite.socialTwitter,
|
|
722
|
+
socialLinkedIn: publishedSite.socialLinkedIn,
|
|
723
|
+
socialYouTube: publishedSite.socialYouTube,
|
|
724
|
+
socialTikTok: publishedSite.socialTikTok,
|
|
470
725
|
}, {
|
|
471
726
|
domains: siteData.value.domains,
|
|
472
727
|
menus: siteData.value.menus,
|
|
@@ -481,9 +736,19 @@ const isSiteDiff = computed(() => {
|
|
|
481
736
|
favicon: siteData.value.favicon,
|
|
482
737
|
menuPosition: siteData.value.menuPosition,
|
|
483
738
|
contactEmail: siteData.value.contactEmail,
|
|
739
|
+
contactPhone: siteData.value.contactPhone,
|
|
484
740
|
metaTitle: siteData.value.metaTitle,
|
|
485
741
|
metaDescription: siteData.value.metaDescription,
|
|
486
742
|
structuredData: siteData.value.structuredData,
|
|
743
|
+
trackingFacebookPixel: siteData.value.trackingFacebookPixel,
|
|
744
|
+
trackingGoogleAnalytics: siteData.value.trackingGoogleAnalytics,
|
|
745
|
+
trackingAdroll: siteData.value.trackingAdroll,
|
|
746
|
+
socialFacebook: siteData.value.socialFacebook,
|
|
747
|
+
socialInstagram: siteData.value.socialInstagram,
|
|
748
|
+
socialTwitter: siteData.value.socialTwitter,
|
|
749
|
+
socialLinkedIn: siteData.value.socialLinkedIn,
|
|
750
|
+
socialYouTube: siteData.value.socialYouTube,
|
|
751
|
+
socialTikTok: siteData.value.socialTikTok,
|
|
487
752
|
})
|
|
488
753
|
}
|
|
489
754
|
return false
|
|
@@ -512,9 +777,19 @@ const discardSiteSettings = async () => {
|
|
|
512
777
|
favicon: publishedSite.favicon || '',
|
|
513
778
|
menuPosition: publishedSite.menuPosition || '',
|
|
514
779
|
contactEmail: publishedSite.contactEmail || '',
|
|
780
|
+
contactPhone: publishedSite.contactPhone || '',
|
|
515
781
|
metaTitle: publishedSite.metaTitle || '',
|
|
516
782
|
metaDescription: publishedSite.metaDescription || '',
|
|
517
783
|
structuredData: publishedSite.structuredData || '',
|
|
784
|
+
trackingFacebookPixel: publishedSite.trackingFacebookPixel || '',
|
|
785
|
+
trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics || '',
|
|
786
|
+
trackingAdroll: publishedSite.trackingAdroll || '',
|
|
787
|
+
socialFacebook: publishedSite.socialFacebook || '',
|
|
788
|
+
socialInstagram: publishedSite.socialInstagram || '',
|
|
789
|
+
socialTwitter: publishedSite.socialTwitter || '',
|
|
790
|
+
socialLinkedIn: publishedSite.socialLinkedIn || '',
|
|
791
|
+
socialYouTube: publishedSite.socialYouTube || '',
|
|
792
|
+
socialTikTok: publishedSite.socialTikTok || '',
|
|
518
793
|
})
|
|
519
794
|
}
|
|
520
795
|
}
|
|
@@ -534,6 +809,19 @@ const publishSite = async () => {
|
|
|
534
809
|
}
|
|
535
810
|
}
|
|
536
811
|
|
|
812
|
+
const publishSiteAndSettings = async () => {
|
|
813
|
+
if (state.publishSiteLoading)
|
|
814
|
+
return
|
|
815
|
+
state.publishSiteLoading = true
|
|
816
|
+
try {
|
|
817
|
+
await publishSiteSettings()
|
|
818
|
+
await publishSite()
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
state.publishSiteLoading = false
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
537
825
|
const pages = computed(() => {
|
|
538
826
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
539
827
|
})
|
|
@@ -588,6 +876,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
588
876
|
metaTitle: publishedPage.metaTitle,
|
|
589
877
|
metaDescription: publishedPage.metaDescription,
|
|
590
878
|
structuredData: publishedPage.structuredData,
|
|
879
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
880
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
881
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
591
882
|
},
|
|
592
883
|
{
|
|
593
884
|
content: draftPage.content,
|
|
@@ -597,6 +888,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
597
888
|
metaTitle: draftPage.metaTitle,
|
|
598
889
|
metaDescription: draftPage.metaDescription,
|
|
599
890
|
structuredData: draftPage.structuredData,
|
|
891
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
892
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
893
|
+
postStructuredData: draftPage.postStructuredData,
|
|
600
894
|
},
|
|
601
895
|
)
|
|
602
896
|
}
|
|
@@ -613,6 +907,8 @@ const setViewMode = (mode) => {
|
|
|
613
907
|
return
|
|
614
908
|
state.viewMode = mode
|
|
615
909
|
state.selectedPostId = ''
|
|
910
|
+
if (mode !== 'submissions')
|
|
911
|
+
state.selectedSubmissionId = ''
|
|
616
912
|
if (props.page)
|
|
617
913
|
router.replace(pageRouteBase.value)
|
|
618
914
|
}
|
|
@@ -662,19 +958,6 @@ watch(pages, (pagesCollection) => {
|
|
|
662
958
|
state.menus = nextMenu
|
|
663
959
|
}, { immediate: true, deep: true })
|
|
664
960
|
|
|
665
|
-
watch(() => state.siteSettings, (open) => {
|
|
666
|
-
if (!open)
|
|
667
|
-
state.logoPickerOpen = false
|
|
668
|
-
if (!open)
|
|
669
|
-
state.logoLightPickerOpen = false
|
|
670
|
-
if (!open)
|
|
671
|
-
state.brandLogoDarkPickerOpen = false
|
|
672
|
-
if (!open)
|
|
673
|
-
state.brandLogoLightPickerOpen = false
|
|
674
|
-
if (!open)
|
|
675
|
-
state.faviconPickerOpen = false
|
|
676
|
-
})
|
|
677
|
-
|
|
678
961
|
watch(() => props.page, (next) => {
|
|
679
962
|
if (next) {
|
|
680
963
|
state.selectedPostId = ''
|
|
@@ -686,6 +969,20 @@ watch(() => props.page, (next) => {
|
|
|
686
969
|
}
|
|
687
970
|
})
|
|
688
971
|
|
|
972
|
+
watch([isViewingSubmissions, sortedSubmissionIds], () => {
|
|
973
|
+
if (!isViewingSubmissions.value)
|
|
974
|
+
return
|
|
975
|
+
const ids = sortedSubmissionIds.value
|
|
976
|
+
if (!ids.length) {
|
|
977
|
+
state.selectedSubmissionId = ''
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
if (!state.selectedSubmissionId || !submissionsMap.value?.[state.selectedSubmissionId]) {
|
|
981
|
+
state.selectedSubmissionId = ids[0]
|
|
982
|
+
markSubmissionRead(ids[0])
|
|
983
|
+
}
|
|
984
|
+
}, { immediate: true })
|
|
985
|
+
|
|
689
986
|
watch(() => state.menus, async (newVal) => {
|
|
690
987
|
if (areEqualNormalized(siteData.value.menus, newVal)) {
|
|
691
988
|
return
|
|
@@ -701,6 +998,8 @@ watch(() => state.menus, async (newVal) => {
|
|
|
701
998
|
const newPage = JSON.parse(JSON.stringify(pageInit))
|
|
702
999
|
for (const [menuName, items] of Object.entries(newVal)) {
|
|
703
1000
|
for (const [index, item] of items.entries()) {
|
|
1001
|
+
if (isExternalLinkEntry(item))
|
|
1002
|
+
continue
|
|
704
1003
|
if (typeof item.item === 'string') {
|
|
705
1004
|
if (item.item === '') {
|
|
706
1005
|
newPage.name = item.name
|
|
@@ -716,9 +1015,11 @@ watch(() => state.menus, async (newVal) => {
|
|
|
716
1015
|
}
|
|
717
1016
|
}
|
|
718
1017
|
}
|
|
719
|
-
if (typeof item.item === 'object') {
|
|
1018
|
+
if (typeof item.item === 'object' && !isExternalLinkEntry(item)) {
|
|
720
1019
|
for (const [subMenuName, subItems] of Object.entries(item.item)) {
|
|
721
1020
|
for (const [subIndex, subItem] of subItems.entries()) {
|
|
1021
|
+
if (isExternalLinkEntry(subItem))
|
|
1022
|
+
continue
|
|
722
1023
|
if (typeof subItem.item === 'string') {
|
|
723
1024
|
if (subItem.item === '') {
|
|
724
1025
|
newPage.name = subItem.name
|
|
@@ -791,6 +1092,9 @@ const isAnyPagesDiff = computed(() => {
|
|
|
791
1092
|
metaTitle: pageData.metaTitle,
|
|
792
1093
|
metaDescription: pageData.metaDescription,
|
|
793
1094
|
structuredData: pageData.structuredData,
|
|
1095
|
+
postMetaTitle: pageData.postMetaTitle,
|
|
1096
|
+
postMetaDescription: pageData.postMetaDescription,
|
|
1097
|
+
postStructuredData: pageData.postStructuredData,
|
|
794
1098
|
},
|
|
795
1099
|
{
|
|
796
1100
|
content: publishedPage.content,
|
|
@@ -800,6 +1104,9 @@ const isAnyPagesDiff = computed(() => {
|
|
|
800
1104
|
metaTitle: publishedPage.metaTitle,
|
|
801
1105
|
metaDescription: publishedPage.metaDescription,
|
|
802
1106
|
structuredData: publishedPage.structuredData,
|
|
1107
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
1108
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
1109
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
803
1110
|
},
|
|
804
1111
|
)) {
|
|
805
1112
|
return true
|
|
@@ -988,18 +1295,45 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
988
1295
|
<FilePenLine class="h-4 w-4" />
|
|
989
1296
|
Posts
|
|
990
1297
|
</edge-shad-button>
|
|
1298
|
+
<edge-shad-button
|
|
1299
|
+
variant="ghost"
|
|
1300
|
+
size="sm"
|
|
1301
|
+
class="h-8 px-4 text-xs gap-2 rounded-full"
|
|
1302
|
+
:class="state.viewMode === 'submissions' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
|
1303
|
+
@click="setViewMode('submissions')"
|
|
1304
|
+
>
|
|
1305
|
+
<Inbox class="h-4 w-4" />
|
|
1306
|
+
Inbox
|
|
1307
|
+
<span
|
|
1308
|
+
v-if="unreadSubmissionsCount"
|
|
1309
|
+
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground"
|
|
1310
|
+
>
|
|
1311
|
+
{{ unreadSubmissionsCount }}
|
|
1312
|
+
</span>
|
|
1313
|
+
</edge-shad-button>
|
|
991
1314
|
</div>
|
|
992
1315
|
</div>
|
|
993
1316
|
<div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
|
|
994
1317
|
<Transition name="fade" mode="out-in">
|
|
995
|
-
<div v-if="isSiteDiff" key="unpublished" class="flex gap-
|
|
996
|
-
<
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1318
|
+
<div v-if="isSiteDiff || isAnyPagesDiff" key="unpublished" class="flex gap-2 items-center">
|
|
1319
|
+
<div class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
|
|
1320
|
+
<CircleAlert class="!text-yellow-800 w-3 h-6" />
|
|
1321
|
+
<span class="font-medium text-[10px]">
|
|
1322
|
+
{{ isSiteDiff ? 'Unpublished Settings' : 'Unpublished Pages' }}
|
|
1323
|
+
</span>
|
|
1324
|
+
</div>
|
|
1325
|
+
<edge-shad-button
|
|
1326
|
+
class="h-8 px-4 text-xs gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
|
|
1327
|
+
:disabled="state.publishSiteLoading"
|
|
1328
|
+
@click="publishSiteAndSettings"
|
|
1329
|
+
>
|
|
1330
|
+
<Loader2 v-if="state.publishSiteLoading" class="h-3.5 w-3.5 animate-spin" />
|
|
1331
|
+
<FolderUp v-else class="h-3.5 w-3.5" />
|
|
1332
|
+
Publish Site
|
|
1333
|
+
</edge-shad-button>
|
|
1000
1334
|
</div>
|
|
1001
1335
|
<div v-else key="published" class="flex gap-1 items-center bg-green-100 text-xs py-1 px-3 text-green-800 rounded">
|
|
1002
|
-
<FileCheck class="!text-green-800 w-3 h-
|
|
1336
|
+
<FileCheck class="!text-green-800 w-3 h-6" />
|
|
1003
1337
|
<span class="font-medium text-[10px]">
|
|
1004
1338
|
Settings Published
|
|
1005
1339
|
</span>
|
|
@@ -1050,7 +1384,147 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1050
1384
|
</div>
|
|
1051
1385
|
<div class="flex-1">
|
|
1052
1386
|
<Transition name="fade" mode="out-in">
|
|
1053
|
-
<div v-if="
|
|
1387
|
+
<div v-if="isViewingSubmissions" class="flex-1 overflow-y-auto p-6">
|
|
1388
|
+
<edge-dashboard
|
|
1389
|
+
:collection="submissionsCollection"
|
|
1390
|
+
query-field="action"
|
|
1391
|
+
query-value="Contact Form"
|
|
1392
|
+
query-operator="=="
|
|
1393
|
+
:filter="state.submissionFilter"
|
|
1394
|
+
:filter-fields="['data.name', 'data.fullName', 'data.firstName', 'data.lastName', 'data.email', 'data.phone', 'data.message', 'data.comments', 'data.notes']"
|
|
1395
|
+
sort-field="timestamp"
|
|
1396
|
+
sort-direction="desc"
|
|
1397
|
+
class="pt-0 flex-1"
|
|
1398
|
+
>
|
|
1399
|
+
<template #header-start>
|
|
1400
|
+
<Inbox class="mr-2 h-4 w-4" />
|
|
1401
|
+
Submissions
|
|
1402
|
+
<!-- <span class="ml-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1403
|
+
Contact Form
|
|
1404
|
+
</span> -->
|
|
1405
|
+
</template>
|
|
1406
|
+
<template #header-center>
|
|
1407
|
+
<div class="w-full px-4 md:px-6">
|
|
1408
|
+
<edge-shad-input
|
|
1409
|
+
v-model="state.submissionFilter"
|
|
1410
|
+
name="submissionFilter"
|
|
1411
|
+
placeholder="Search submissions..."
|
|
1412
|
+
class="w-full"
|
|
1413
|
+
/>
|
|
1414
|
+
</div>
|
|
1415
|
+
</template>
|
|
1416
|
+
<template #header-end="slotProps">
|
|
1417
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1418
|
+
{{ slotProps.recordCount }} total • {{ unreadSubmissionsCount }} unread
|
|
1419
|
+
</span>
|
|
1420
|
+
</template>
|
|
1421
|
+
<template #list="slotProps">
|
|
1422
|
+
<div class="grid gap-4 pt-4 w-full md:grid-cols-[320px_minmax(0,1fr)]">
|
|
1423
|
+
<div class="space-y-2">
|
|
1424
|
+
<div
|
|
1425
|
+
v-for="item in slotProps.filtered"
|
|
1426
|
+
:key="item.docId"
|
|
1427
|
+
role="button"
|
|
1428
|
+
tabindex="0"
|
|
1429
|
+
class="group rounded-lg border p-3 text-left transition hover:border-primary/60 hover:bg-muted/60"
|
|
1430
|
+
:class="state.selectedSubmissionId === item.docId ? 'border-primary/70 bg-muted/70 shadow-sm' : 'border-border/60 bg-card'"
|
|
1431
|
+
@click="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
|
|
1432
|
+
@keyup.enter="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
|
|
1433
|
+
>
|
|
1434
|
+
<div class="flex items-start justify-between gap-2">
|
|
1435
|
+
<div class="min-w-0">
|
|
1436
|
+
<div class="truncate text-sm font-semibold text-foreground">
|
|
1437
|
+
{{ getSubmissionLabel(item.data) }}
|
|
1438
|
+
</div>
|
|
1439
|
+
<div v-if="item.data?.pageName" class="truncate text-xs text-muted-foreground">
|
|
1440
|
+
{{ item.data.pageName }}
|
|
1441
|
+
</div>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
1444
|
+
<span v-if="isSubmissionUnread(item)" class="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
|
|
1445
|
+
Unread
|
|
1446
|
+
</span>
|
|
1447
|
+
<span>{{ formatSubmissionTimestamp(item.timestamp) }}</span>
|
|
1448
|
+
</div>
|
|
1449
|
+
</div>
|
|
1450
|
+
<div v-if="getSubmissionMessage(item.data)" class="mt-2 text-xs text-muted-foreground line-clamp-2">
|
|
1451
|
+
{{ getSubmissionMessage(item.data) }}
|
|
1452
|
+
</div>
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
<div>
|
|
1456
|
+
<Card v-if="selectedSubmission" class="border border-border/70 bg-card/95 shadow-sm">
|
|
1457
|
+
<CardHeader class="flex flex-col gap-2">
|
|
1458
|
+
<div class="flex flex-wrap items-start justify-between gap-2">
|
|
1459
|
+
<div>
|
|
1460
|
+
<CardTitle class="text-xl">
|
|
1461
|
+
{{ getSubmissionLabel(selectedSubmission.data) }}
|
|
1462
|
+
</CardTitle>
|
|
1463
|
+
<CardDescription class="text-xs">
|
|
1464
|
+
{{ formatSubmissionTimestamp(selectedSubmission.timestamp) }}
|
|
1465
|
+
</CardDescription>
|
|
1466
|
+
</div>
|
|
1467
|
+
<div class="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1468
|
+
<span>
|
|
1469
|
+
{{ selectedSubmission.data?.pageName || selectedSubmission.data?.pageId || 'Site submission' }}
|
|
1470
|
+
</span>
|
|
1471
|
+
<edge-shad-button
|
|
1472
|
+
v-if="isSubmissionUnread(selectedSubmission)"
|
|
1473
|
+
size="sm"
|
|
1474
|
+
variant="outline"
|
|
1475
|
+
class="h-7 gap-2 text-[11px]"
|
|
1476
|
+
@click="markSubmissionRead(selectedSubmission.docId)"
|
|
1477
|
+
>
|
|
1478
|
+
<MailOpen class="h-3.5 w-3.5" />
|
|
1479
|
+
Mark read
|
|
1480
|
+
</edge-shad-button>
|
|
1481
|
+
<edge-shad-button
|
|
1482
|
+
v-else
|
|
1483
|
+
size="sm"
|
|
1484
|
+
variant="outline"
|
|
1485
|
+
class="h-7 gap-2 text-[11px]"
|
|
1486
|
+
@click="markSubmissionUnread(selectedSubmission.docId)"
|
|
1487
|
+
>
|
|
1488
|
+
<Mail class="h-3.5 w-3.5" />
|
|
1489
|
+
Mark unread
|
|
1490
|
+
</edge-shad-button>
|
|
1491
|
+
</div>
|
|
1492
|
+
</div>
|
|
1493
|
+
</CardHeader>
|
|
1494
|
+
<CardContent class="space-y-4">
|
|
1495
|
+
<div
|
|
1496
|
+
v-if="getSubmissionMessage(selectedSubmission.data)"
|
|
1497
|
+
class="rounded-lg border border-border/60 bg-muted/40 p-3 text-sm text-foreground"
|
|
1498
|
+
>
|
|
1499
|
+
{{ getSubmissionMessage(selectedSubmission.data) }}
|
|
1500
|
+
</div>
|
|
1501
|
+
<div class="grid gap-3 md:grid-cols-2">
|
|
1502
|
+
<div
|
|
1503
|
+
v-for="entry in collectSubmissionEntries(selectedSubmission.data)"
|
|
1504
|
+
:key="entry.key"
|
|
1505
|
+
class="rounded-lg border border-border/60 bg-background p-3"
|
|
1506
|
+
>
|
|
1507
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1508
|
+
{{ formatSubmissionKey(entry.key) }}
|
|
1509
|
+
</div>
|
|
1510
|
+
<div class="mt-1 text-sm text-foreground break-words">
|
|
1511
|
+
{{ formatSubmissionValue(entry.value) }}
|
|
1512
|
+
</div>
|
|
1513
|
+
</div>
|
|
1514
|
+
</div>
|
|
1515
|
+
</CardContent>
|
|
1516
|
+
</Card>
|
|
1517
|
+
<Card v-else class="border border-dashed border-border/80 bg-muted/30">
|
|
1518
|
+
<CardContent class="py-12 text-center text-sm text-muted-foreground">
|
|
1519
|
+
Select a submission to view details.
|
|
1520
|
+
</CardContent>
|
|
1521
|
+
</Card>
|
|
1522
|
+
</div>
|
|
1523
|
+
</div>
|
|
1524
|
+
</template>
|
|
1525
|
+
</edge-dashboard>
|
|
1526
|
+
</div>
|
|
1527
|
+
<div v-else-if="isEditingPost" class="w-full h-full">
|
|
1054
1528
|
<edge-cms-posts
|
|
1055
1529
|
mode="editor"
|
|
1056
1530
|
:site="props.site"
|
|
@@ -1089,7 +1563,7 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1089
1563
|
</ResizablePanel>
|
|
1090
1564
|
<ResizablePanel ref="mainPanel">
|
|
1091
1565
|
<Transition name="fade" mode="out-in">
|
|
1092
|
-
<div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-
|
|
1566
|
+
<div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-100px)] overflow-y-auto w-full">
|
|
1093
1567
|
<NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
|
|
1094
1568
|
</div>
|
|
1095
1569
|
<div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
|
|
@@ -1134,324 +1608,24 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1134
1608
|
>
|
|
1135
1609
|
<template #main="slotProps">
|
|
1136
1610
|
<div class="p-6 h-[calc(100vh-140px)] overflow-y-auto">
|
|
1137
|
-
<
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
SEO
|
|
1150
|
-
</TabsTrigger>
|
|
1151
|
-
</TabsList>
|
|
1152
|
-
<TabsContent value="general" class="pt-4 space-y-4">
|
|
1153
|
-
<edge-shad-input
|
|
1154
|
-
v-model="slotProps.workingDoc.name"
|
|
1155
|
-
name="name"
|
|
1156
|
-
label="Name"
|
|
1157
|
-
placeholder="Enter name"
|
|
1158
|
-
class="w-full"
|
|
1159
|
-
/>
|
|
1160
|
-
<edge-shad-tags
|
|
1161
|
-
v-model="slotProps.workingDoc.domains"
|
|
1162
|
-
name="domains"
|
|
1163
|
-
label="Domains"
|
|
1164
|
-
placeholder="Add or remove domains"
|
|
1165
|
-
class="w-full"
|
|
1166
|
-
/>
|
|
1167
|
-
<edge-shad-input
|
|
1168
|
-
v-model="slotProps.workingDoc.contactEmail"
|
|
1169
|
-
name="contactEmail"
|
|
1170
|
-
label="Contact Email"
|
|
1171
|
-
placeholder="name@example.com"
|
|
1172
|
-
class="w-full"
|
|
1173
|
-
/>
|
|
1174
|
-
<edge-shad-select-tags
|
|
1175
|
-
v-if="Object.keys(orgUsers).length > 0 && isAdmin"
|
|
1176
|
-
v-model="slotProps.workingDoc.users" :disabled="!edgeGlobal.isAdminGlobal(edgeFirebase).value"
|
|
1177
|
-
:items="userOptions" name="users" label="Users"
|
|
1178
|
-
item-title="label" item-value="value" placeholder="Select users" class="w-full" :multiple="true"
|
|
1179
|
-
/>
|
|
1180
|
-
<p v-else class="text-sm text-muted-foreground">
|
|
1181
|
-
No organization users available for this site.
|
|
1182
|
-
</p>
|
|
1183
|
-
</TabsContent>
|
|
1184
|
-
<TabsContent value="appearance" class="pt-4 space-y-4">
|
|
1185
|
-
<edge-shad-select-tags
|
|
1186
|
-
v-if="isAdmin"
|
|
1187
|
-
:model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
|
|
1188
|
-
name="allowedThemes"
|
|
1189
|
-
label="Allowed Themes"
|
|
1190
|
-
placeholder="Select allowed themes"
|
|
1191
|
-
class="w-full"
|
|
1192
|
-
:items="themeOptions"
|
|
1193
|
-
item-title="label"
|
|
1194
|
-
item-value="value"
|
|
1195
|
-
@update:model-value="(value) => {
|
|
1196
|
-
const normalized = Array.isArray(value) ? value : []
|
|
1197
|
-
slotProps.workingDoc.allowedThemes = normalized
|
|
1198
|
-
if (normalized.length && !normalized.includes(slotProps.workingDoc.theme)) {
|
|
1199
|
-
slotProps.workingDoc.theme = normalized[0] || ''
|
|
1200
|
-
}
|
|
1201
|
-
}"
|
|
1202
|
-
/>
|
|
1203
|
-
<edge-shad-select
|
|
1204
|
-
:model-value="slotProps.workingDoc.theme || ''"
|
|
1205
|
-
name="theme"
|
|
1206
|
-
label="Theme"
|
|
1207
|
-
placeholder="Select a theme"
|
|
1208
|
-
class="w-full"
|
|
1209
|
-
:items="themeItemsForAllowed(slotProps.workingDoc.allowedThemes, slotProps.workingDoc.theme)"
|
|
1210
|
-
item-title="label"
|
|
1211
|
-
item-value="value"
|
|
1212
|
-
@update:model-value="value => (slotProps.workingDoc.theme = value || '')"
|
|
1213
|
-
/>
|
|
1214
|
-
<edge-shad-select
|
|
1215
|
-
:model-value="slotProps.workingDoc.menuPosition || ''"
|
|
1216
|
-
name="menuPosition"
|
|
1217
|
-
label="Menu Position"
|
|
1218
|
-
placeholder="Select menu position"
|
|
1219
|
-
class="w-full"
|
|
1220
|
-
:items="menuPositionOptions"
|
|
1221
|
-
item-title="label"
|
|
1222
|
-
item-value="value"
|
|
1223
|
-
@update:model-value="value => (slotProps.workingDoc.menuPosition = value || '')"
|
|
1224
|
-
/>
|
|
1225
|
-
</TabsContent>
|
|
1226
|
-
<TabsContent value="branding" class="pt-4 space-y-4">
|
|
1227
|
-
<div class="space-y-2">
|
|
1228
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1229
|
-
Dark logo
|
|
1230
|
-
<edge-shad-button
|
|
1231
|
-
type="button"
|
|
1232
|
-
variant="link"
|
|
1233
|
-
class="px-0 h-auto text-sm"
|
|
1234
|
-
@click="state.logoPickerOpen = !state.logoPickerOpen"
|
|
1235
|
-
>
|
|
1236
|
-
{{ state.logoPickerOpen ? 'Hide picker' : 'Select logo' }}
|
|
1237
|
-
</edge-shad-button>
|
|
1238
|
-
</label>
|
|
1239
|
-
<div class="flex items-center gap-4">
|
|
1240
|
-
<div v-if="slotProps.workingDoc.logo" class="flex items-center gap-3">
|
|
1241
|
-
<img :src="slotProps.workingDoc.logo" alt="Logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
|
|
1242
|
-
<edge-shad-button
|
|
1243
|
-
type="button"
|
|
1244
|
-
variant="ghost"
|
|
1245
|
-
class="h-8"
|
|
1246
|
-
@click="slotProps.workingDoc.logo = ''"
|
|
1247
|
-
>
|
|
1248
|
-
Remove
|
|
1249
|
-
</edge-shad-button>
|
|
1250
|
-
</div>
|
|
1251
|
-
<span v-else class="text-sm text-muted-foreground italic">No logo selected</span>
|
|
1252
|
-
</div>
|
|
1253
|
-
<div v-if="state.logoPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1254
|
-
<edge-cms-media-manager
|
|
1255
|
-
:site="props.site"
|
|
1256
|
-
:select-mode="true"
|
|
1257
|
-
:default-tags="['Logos']"
|
|
1258
|
-
@select="(url) => {
|
|
1259
|
-
slotProps.workingDoc.logo = url
|
|
1260
|
-
state.logoPickerOpen = false
|
|
1261
|
-
}"
|
|
1262
|
-
/>
|
|
1263
|
-
</div>
|
|
1264
|
-
</div>
|
|
1265
|
-
<div class="space-y-2">
|
|
1266
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1267
|
-
Light logo
|
|
1268
|
-
<edge-shad-button
|
|
1269
|
-
type="button"
|
|
1270
|
-
variant="link"
|
|
1271
|
-
class="px-0 h-auto text-sm"
|
|
1272
|
-
@click="state.logoLightPickerOpen = !state.logoLightPickerOpen"
|
|
1273
|
-
>
|
|
1274
|
-
{{ state.logoLightPickerOpen ? 'Hide picker' : 'Select logo' }}
|
|
1275
|
-
</edge-shad-button>
|
|
1276
|
-
</label>
|
|
1277
|
-
<div class="flex items-center gap-4">
|
|
1278
|
-
<div v-if="slotProps.workingDoc.logoLight" class="flex items-center gap-3">
|
|
1279
|
-
<img :src="slotProps.workingDoc.logoLight" alt="Light logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
|
|
1280
|
-
<edge-shad-button
|
|
1281
|
-
type="button"
|
|
1282
|
-
variant="ghost"
|
|
1283
|
-
class="h-8"
|
|
1284
|
-
@click="slotProps.workingDoc.logoLight = ''"
|
|
1285
|
-
>
|
|
1286
|
-
Remove
|
|
1287
|
-
</edge-shad-button>
|
|
1288
|
-
</div>
|
|
1289
|
-
<span v-else class="text-sm text-muted-foreground italic">No light logo selected</span>
|
|
1290
|
-
</div>
|
|
1291
|
-
<div v-if="state.logoLightPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1292
|
-
<edge-cms-media-manager
|
|
1293
|
-
:site="props.site"
|
|
1294
|
-
:select-mode="true"
|
|
1295
|
-
:default-tags="['Logos']"
|
|
1296
|
-
@select="(url) => {
|
|
1297
|
-
slotProps.workingDoc.logoLight = url
|
|
1298
|
-
state.logoLightPickerOpen = false
|
|
1299
|
-
}"
|
|
1300
|
-
/>
|
|
1301
|
-
</div>
|
|
1302
|
-
</div>
|
|
1303
|
-
<div v-if="isAdmin" class="space-y-4 border border-dashed rounded-lg p-4">
|
|
1304
|
-
<div class="text-sm font-semibold text-foreground">
|
|
1305
|
-
Umbrella Brand
|
|
1306
|
-
</div>
|
|
1307
|
-
<div class="space-y-2">
|
|
1308
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1309
|
-
Dark brand logo
|
|
1310
|
-
<edge-shad-button
|
|
1311
|
-
type="button"
|
|
1312
|
-
variant="link"
|
|
1313
|
-
class="px-0 h-auto text-sm"
|
|
1314
|
-
@click="state.brandLogoDarkPickerOpen = !state.brandLogoDarkPickerOpen"
|
|
1315
|
-
>
|
|
1316
|
-
{{ state.brandLogoDarkPickerOpen ? 'Hide picker' : 'Select logo' }}
|
|
1317
|
-
</edge-shad-button>
|
|
1318
|
-
</label>
|
|
1319
|
-
<div class="flex items-center gap-4">
|
|
1320
|
-
<div v-if="slotProps.workingDoc.brandLogoDark" class="flex items-center gap-3">
|
|
1321
|
-
<img :src="slotProps.workingDoc.brandLogoDark" alt="Brand dark logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
|
|
1322
|
-
<edge-shad-button
|
|
1323
|
-
type="button"
|
|
1324
|
-
variant="ghost"
|
|
1325
|
-
class="h-8"
|
|
1326
|
-
@click="slotProps.workingDoc.brandLogoDark = ''"
|
|
1327
|
-
>
|
|
1328
|
-
Remove
|
|
1329
|
-
</edge-shad-button>
|
|
1330
|
-
</div>
|
|
1331
|
-
<span v-else class="text-sm text-muted-foreground italic">No brand dark logo selected</span>
|
|
1332
|
-
</div>
|
|
1333
|
-
<div v-if="state.brandLogoDarkPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1334
|
-
<edge-cms-media-manager
|
|
1335
|
-
:site="props.site"
|
|
1336
|
-
:select-mode="true"
|
|
1337
|
-
:default-tags="['Logos']"
|
|
1338
|
-
@select="(url) => {
|
|
1339
|
-
slotProps.workingDoc.brandLogoDark = url
|
|
1340
|
-
state.brandLogoDarkPickerOpen = false
|
|
1341
|
-
}"
|
|
1342
|
-
/>
|
|
1343
|
-
</div>
|
|
1344
|
-
</div>
|
|
1345
|
-
<div class="space-y-2">
|
|
1346
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1347
|
-
Light brand logo
|
|
1348
|
-
<edge-shad-button
|
|
1349
|
-
type="button"
|
|
1350
|
-
variant="link"
|
|
1351
|
-
class="px-0 h-auto text-sm"
|
|
1352
|
-
@click="state.brandLogoLightPickerOpen = !state.brandLogoLightPickerOpen"
|
|
1353
|
-
>
|
|
1354
|
-
{{ state.brandLogoLightPickerOpen ? 'Hide picker' : 'Select logo' }}
|
|
1355
|
-
</edge-shad-button>
|
|
1356
|
-
</label>
|
|
1357
|
-
<div class="flex items-center gap-4">
|
|
1358
|
-
<div v-if="slotProps.workingDoc.brandLogoLight" class="flex items-center gap-3">
|
|
1359
|
-
<img :src="slotProps.workingDoc.brandLogoLight" alt="Brand light logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
|
|
1360
|
-
<edge-shad-button
|
|
1361
|
-
type="button"
|
|
1362
|
-
variant="ghost"
|
|
1363
|
-
class="h-8"
|
|
1364
|
-
@click="slotProps.workingDoc.brandLogoLight = ''"
|
|
1365
|
-
>
|
|
1366
|
-
Remove
|
|
1367
|
-
</edge-shad-button>
|
|
1368
|
-
</div>
|
|
1369
|
-
<span v-else class="text-sm text-muted-foreground italic">No brand light logo selected</span>
|
|
1370
|
-
</div>
|
|
1371
|
-
<div v-if="state.brandLogoLightPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1372
|
-
<edge-cms-media-manager
|
|
1373
|
-
:site="props.site"
|
|
1374
|
-
:select-mode="true"
|
|
1375
|
-
:default-tags="['Logos']"
|
|
1376
|
-
@select="(url) => {
|
|
1377
|
-
slotProps.workingDoc.brandLogoLight = url
|
|
1378
|
-
state.brandLogoLightPickerOpen = false
|
|
1379
|
-
}"
|
|
1380
|
-
/>
|
|
1381
|
-
</div>
|
|
1382
|
-
</div>
|
|
1383
|
-
</div>
|
|
1384
|
-
<div class="space-y-2">
|
|
1385
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1386
|
-
Favicon
|
|
1387
|
-
<edge-shad-button
|
|
1388
|
-
type="button"
|
|
1389
|
-
variant="link"
|
|
1390
|
-
class="px-0 h-auto text-sm"
|
|
1391
|
-
@click="state.faviconPickerOpen = !state.faviconPickerOpen"
|
|
1392
|
-
>
|
|
1393
|
-
{{ state.faviconPickerOpen ? 'Hide picker' : 'Select favicon' }}
|
|
1394
|
-
</edge-shad-button>
|
|
1395
|
-
</label>
|
|
1396
|
-
<div class="flex items-center gap-4">
|
|
1397
|
-
<div v-if="slotProps.workingDoc.favicon" class="flex items-center gap-3">
|
|
1398
|
-
<img :src="slotProps.workingDoc.favicon" alt="Favicon preview" class="h-12 w-12 rounded-md border border-border bg-muted object-contain">
|
|
1399
|
-
<edge-shad-button
|
|
1400
|
-
type="button"
|
|
1401
|
-
variant="ghost"
|
|
1402
|
-
class="h-8"
|
|
1403
|
-
@click="slotProps.workingDoc.favicon = ''"
|
|
1404
|
-
>
|
|
1405
|
-
Remove
|
|
1406
|
-
</edge-shad-button>
|
|
1407
|
-
</div>
|
|
1408
|
-
<span v-else class="text-sm text-muted-foreground italic">No favicon selected</span>
|
|
1409
|
-
</div>
|
|
1410
|
-
<div v-if="state.faviconPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1411
|
-
<edge-cms-media-manager
|
|
1412
|
-
:site="props.site"
|
|
1413
|
-
:select-mode="true"
|
|
1414
|
-
:default-tags="['Logos']"
|
|
1415
|
-
@select="(url) => {
|
|
1416
|
-
slotProps.workingDoc.favicon = url
|
|
1417
|
-
state.faviconPickerOpen = false
|
|
1418
|
-
}"
|
|
1419
|
-
/>
|
|
1420
|
-
</div>
|
|
1421
|
-
</div>
|
|
1422
|
-
</TabsContent>
|
|
1423
|
-
<TabsContent value="seo" class="pt-4">
|
|
1424
|
-
<div class="space-y-4">
|
|
1425
|
-
<p class="text-sm text-muted-foreground">
|
|
1426
|
-
Default settings if the information is not entered on the page.
|
|
1427
|
-
</p>
|
|
1428
|
-
<edge-shad-input
|
|
1429
|
-
v-model="slotProps.workingDoc.metaTitle"
|
|
1430
|
-
label="Meta Title"
|
|
1431
|
-
name="metaTitle"
|
|
1432
|
-
/>
|
|
1433
|
-
<edge-shad-textarea
|
|
1434
|
-
v-model="slotProps.workingDoc.metaDescription"
|
|
1435
|
-
label="Meta Description"
|
|
1436
|
-
name="metaDescription"
|
|
1437
|
-
/>
|
|
1438
|
-
<edge-cms-code-editor
|
|
1439
|
-
v-model="slotProps.workingDoc.structuredData"
|
|
1440
|
-
title="Structured Data (JSON-LD)"
|
|
1441
|
-
language="json"
|
|
1442
|
-
name="structuredData"
|
|
1443
|
-
height="300px"
|
|
1444
|
-
class="mb-4 w-full"
|
|
1445
|
-
/>
|
|
1446
|
-
</div>
|
|
1447
|
-
</TabsContent>
|
|
1448
|
-
</Tabs>
|
|
1611
|
+
<edge-cms-site-settings-form
|
|
1612
|
+
:settings="slotProps.workingDoc"
|
|
1613
|
+
:theme-options="themeOptions"
|
|
1614
|
+
:user-options="userOptions"
|
|
1615
|
+
:has-users="Object.keys(orgUsers).length > 0"
|
|
1616
|
+
:show-users="true"
|
|
1617
|
+
:show-theme-fields="true"
|
|
1618
|
+
:is-admin="isAdmin"
|
|
1619
|
+
:enable-media-picker="true"
|
|
1620
|
+
:site-id="props.site"
|
|
1621
|
+
:domain-error="domainError"
|
|
1622
|
+
/>
|
|
1449
1623
|
</div>
|
|
1450
1624
|
<SheetFooter class="pt-2 flex justify-between">
|
|
1451
1625
|
<edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
|
|
1452
1626
|
Cancel
|
|
1453
1627
|
</edge-shad-button>
|
|
1454
|
-
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1628
|
+
<edge-shad-button :disabled="slotProps.submitting || isJsonInvalid(slotProps.workingDoc?.structuredData)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1455
1629
|
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1456
1630
|
Update
|
|
1457
1631
|
</edge-shad-button>
|