@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.
Files changed (35) hide show
  1. package/edge/components/auth/register.vue +51 -0
  2. package/edge/components/cms/block.vue +363 -42
  3. package/edge/components/cms/blockEditor.vue +50 -3
  4. package/edge/components/cms/codeEditor.vue +39 -2
  5. package/edge/components/cms/htmlContent.vue +10 -2
  6. package/edge/components/cms/init_blocks/footer.html +111 -19
  7. package/edge/components/cms/init_blocks/image.html +8 -0
  8. package/edge/components/cms/init_blocks/post_content.html +3 -2
  9. package/edge/components/cms/init_blocks/post_title_header.html +8 -6
  10. package/edge/components/cms/init_blocks/posts_list.html +6 -5
  11. package/edge/components/cms/mediaCard.vue +13 -2
  12. package/edge/components/cms/mediaManager.vue +35 -5
  13. package/edge/components/cms/menu.vue +384 -61
  14. package/edge/components/cms/optionsSelect.vue +20 -3
  15. package/edge/components/cms/page.vue +160 -18
  16. package/edge/components/cms/site.vue +548 -374
  17. package/edge/components/cms/siteSettingsForm.vue +623 -0
  18. package/edge/components/cms/themeDefaultMenu.vue +258 -22
  19. package/edge/components/cms/themeEditor.vue +95 -11
  20. package/edge/components/editor.vue +1 -0
  21. package/edge/components/formSubtypes/myOrgs.vue +112 -1
  22. package/edge/components/imagePicker.vue +126 -0
  23. package/edge/components/myAccount.vue +1 -0
  24. package/edge/components/myProfile.vue +345 -61
  25. package/edge/components/orgSwitcher.vue +1 -1
  26. package/edge/components/organizationMembers.vue +620 -235
  27. package/edge/components/shad/html.vue +6 -0
  28. package/edge/components/shad/number.vue +2 -2
  29. package/edge/components/sideBar.vue +7 -4
  30. package/edge/components/sideBarContent.vue +1 -1
  31. package/edge/components/userMenu.vue +50 -14
  32. package/edge/composables/global.ts +4 -1
  33. package/edge/composables/siteSettingsTemplate.js +79 -0
  34. package/edge/composables/structuredDataTemplates.js +36 -0
  35. 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 || id,
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: templateDoc?.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 (!themeMenus)
387
- return
388
- const templatePages = await ensureTemplatePagesSnapshot()
389
- const usedSlugs = new Set()
390
- const seededMenus = ensureMenuBuckets(themeMenus)
391
- seededMenus['Site Root'] = await duplicateEntriesWithPages(seededMenus['Site Root'], { templatePages, siteId, usedSlugs })
392
- seededMenus['Not In Menu'] = await duplicateEntriesWithPages(seededMenus['Not In Menu'], { templatePages, siteId, usedSlugs })
393
- await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, siteId, { menus: seededMenus })
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-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
996
- <CircleAlert class="!text-yellow-800 w-3 h-3" />
997
- <span class="font-medium text-[10px]">
998
- Unpublished Settings
999
- </span>
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-3" />
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="isEditingPost" class="w-full h-full">
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-50px)] overflow-y-auto w-full">
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
- <Tabs class="w-full" default-value="general">
1138
- <TabsList class="w-full flex flex-wrap gap-2 bg-muted/40 p-1 rounded-lg">
1139
- <TabsTrigger value="general" class="text-md uppercase font-medium">
1140
- General
1141
- </TabsTrigger>
1142
- <TabsTrigger value="appearance" class="text-md uppercase font-medium">
1143
- Appearance
1144
- </TabsTrigger>
1145
- <TabsTrigger value="branding" class="text-md uppercase font-medium">
1146
- Branding
1147
- </TabsTrigger>
1148
- <TabsTrigger value="seo" class="text-md uppercase font-medium">
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>